Lazy loaded image
并发编程
🗒️线程池
Words 3460Read Time 9 min
2025-8-12
2025-9-12
type
status
date
slug
summary
tags
category
icon
password
原文

1. 功能和使用场景

功能

  • 降低资源消耗→通过池化技术重复利用已有创建的线程,降低线程创建和销毁造成的损耗
  • 提高响应速度→任务提交时,无须等待线程池的创建即可立即执行
  • 提高线程的管理性→线程是稀缺资源,如果无限制创建,不仅会消耗系统资源,还会因为线程的不合理分布导致资源调度失衡。使用线程池可以统一的分配、调度和监控
  • 提供更强大的功能→ 线程池具备扩展性,允许开发人员增加更多的功能。比如延时定时线程池ScheduledThreadPoolExceutor,允许任务延期执行或定期执行。

常用场景

  • 内存池 → 预先申请内存,提升申请内存速度,减少内存碎片。
  • 连接池 → 预先申请数据库连接,提升申请连接速度,降低系统开销。
  • 实例池 → 循环使用对象,减少资源在初始化和释放时的昂贵资源。

2. 执行流程

3.参数说明

3.1 构造器

3.2 入参

  1. corePoolSize
线程池的最小线程数量,即使线程处于空闲状态,也不会被销毁,除非设置了allowCoreThreadTimeOut(true)
2.maximumPoolSize
最大线程数
  1. keepAliveTime
一个线程如果处于空闲状态,并且当前的数量大于corePoolSize,那么在指定时间后,这个空闲线程会被销毁,这个指定时间由keepAliveTime决定
  1. unit
keepAliveTime 的计量单位
3.workQueue
工作队列,任务提交后会加入到工作队列。
jdk提供了四种工作队列
  • ArrayBlockingQueue
基于数组的有界阻塞队列,按FIFO排序,新任务进来会放入队尾。有界的数组可以防止资源被耗尽。
  • LinkedBlockingQueue
基于链表的无界阻塞队列(最大容量为Integer.MAX),按照FIFO排序。由于该队列的近似无界性,当线程池线程数达到corePoolSize后,再有新任务进来,会一直存在该队列,而不去创建线程直到maximumPoolSize,因此使用该队列时,maximumPoolSize是不起作用的
  • SynchronousQueue
一个不缓存任务的阻塞队列,生产者放入一个任务必须等到消费者取出这个任务。也就是新任务进来时不会缓存,而是直接调度这个任务。如果没有可用线程,则创建新线程直到maximumPoolSize。
  • PriorityBlockingQueue
具有优先级的无界阻塞队列,优先级通过参数Comparator实现。
  1. threadFactory
创建一个新线程时使用的工厂,可以用来设定线程名,是否为daemon线程,优先级等。
  1. handler(拒绝策略)
拒绝策略。当工作队列的任务队列已达最大限制,并且线程池中的线程数量也达到最大限制,这时如果有新任务提交进来,则开始执行拒绝策略
  • CallerRunsPolicy
    • 在调用线程中,直接拒绝任务的run方法,除非线程池已经shutdown,则直接抛弃任务
  • AbortPolicy(默认策略)
    • 直接丢弃任务抛出RejectedExecutionException异常。
       
  • DiscardPolicy
    • 丢弃任务,什么也不做。
  • DiscardOldestPolicy
    • 抛弃最早进入队列的那个任务,然后尝试把这次的拒绝的任务放入队列。
  • 自定义策略
    • 自定义类实现RejectedExecutionHandler,实现rejectedExecution方法。

4. 线程池的状态

5. 如何关闭线程池

 
  • shutdown() :JUC提供的一种有序关闭线程池的方法,该方法会等待当前的工作队列中的剩余任务全部执行完成之后执行关闭操作,此方法被调用后会将线程池的状态设置为SHUTDOWN,线程池也不再接受新的任务。
  • shutdownNow() :JUC提供的一种立即关闭线程池的方法,此方法会打断正在执行的工作线程,并且会清空当前工作队列中的剩余任务,返回的是尚未执行的任务。
  • awaitTermination(): 等待线程池完成关闭。在调用线程池的shutdown()、shutdownNow()会立即返回,不会一直等到线程池完成关闭。

5.1 何优雅的关闭线程池

大家可以结合shutdown()、shutdownNow()、awaitTermination()三个方法优雅地关闭一个线程池,大致分为以下几步:
(1)执行shutdown()方法,拒绝新任务的提交,并等待所有任务有序地执行完毕。
(2)执行awaitTermination(long timeout , TimeUnit unit)方法,指定超时时间,判断是否已经关闭所有任务,线程池关闭完成。
(3)如果awaitTermination()方法返回false,或者被中断,就调用shutDownNow()方法立即关闭线程池所有任务。
(4)补充执行awaitTermination(long timeout,TimeUnit unit)方法,判断线程池是否关闭完成。如果超时,就可以进入循环关闭,循环一定的次数(如1000次),不断关闭线程池,直到其关闭或者循环结束。

6.JDK提供的线程池

  • newCachedThreadPool
    • 缓存线程池,适用于执行大量并发短期的异步任务。不过任务的负载要“轻”
  • newFixedThreadPool
    • 定长线程池。适用于执行负载重,cpu使用频率高的任务。这个是为了防止太多线程进行大量的线程切换。
  • newSingleThreadExecutor
    • 单线程线程池,一般用于执行需要按照执行按照指定顺序的任务。
  • newScheduledThreadPool
    • 周期定长线程池,一般是周期性的任务,不过可以使用其他的替代。

7. 线程池如何监控

 
使用ThreadPoolExecutor 提供的方法
 

8. 如何配置线程池

8.1 IO密集型 VS 计算密集型

  • 计算密集型 → 程序系统大部分在做计算、逻辑判断、循环导致cpu占用率很高的情况
  • IO密集型 → 频繁网络传输、读取硬盘及其他io设备称之为io密集型
  • 线程池大小=Cpu数量+1
NCpu = 处理器核的数目 可通过Runtime.getRuntime().availableProcessors()得到
UCpu = 期望的CPU利用率(该值在0-1之间)
W/C是等待时间于计算时间的比率(IO耗时/CPU耗时)
在实际工作中公式过于理想化,可以通过公式先确认一个数字,然后根据Apm等性能分析工具再压测以调试到合适的数字。
总结:
  • IO 密集型任务:由于线程并不是一直在运行,所以可以尽可能的多配置线程,比如 CPU 个数 * 2
  • CPU 密集型任务(大量复杂的运算)应当分配较少的线程,比如 CPU 个数相当的大小。

Runtime.getRuntime().availableProcessors()

在 Jdk 中常通过该方法获取CPU的核心数,容器化部署,JDK 8u131 之前可能无法获取容器的 CPU 限制(如限制容器只能使用 1 个核心,而宿主机有 8 核心,该方法返回了 8)。

9.如何动态调整线程池

基于配置中心去更改,需要监听 Nacos 或其他配置中心配置更改的事件或消息。
线程池的核心线程数、最大线程数、keepAliveTime、拒绝策略等都可以更改。凡是 ThreadPoolExecutor 提供了 set 方法的都可以修改。
JDK 提供的阻塞队列都不支持动态扩容,如果队列容量要动态变更需要自己实现 BlockingQueue 接口实现扩容的逻辑。当然这个 BlockingQueue 是需要实现线程安全的。
上一篇
G1
下一篇
静态变量过大导致OOM

Comments
Loading...