[TOC]
实现原理
线程池的组成

提交Job的处理流程:

核心代码
1 | public void execute(Runnable command) { |
工作线程worker数量低于核心工作线程数corePoolSize时会优先创建一个工作线程worker处理job, 处理成功则返回.工作线程worker数量高于核心工作线程数时会优先把job放入到任务队列, 放入队列成功时处理结束.- 入队失败会识别工作线程数是否还小于
最大工作线程数maximumPoolsize, 小于的话也会新创建一个工作线程worker处理job. - 饱和策略
此外, 运行过程中, 更新核心工作线程数时, 若发现扩容, 会增加工作线程数.
备注:
- Java中的线程与操作系统的线程是一一对应的
- 添加新线程需要获得全局锁
private final ReentrantLock mainLock = new ReentrantLock();, 因此, 当工作线程work数量大于核心工作线程数corePoolSize时, 优先放入任务队列 - 只要工作线程达不到
corePoolSize, 不管是否线程空闲, 都会创建新线程 - 调用
prestartAllCoreThreads会初始化所有的核心线程, 没有预热期, 响应快, 但空载浪费资源 - 向
任务队列添加任务, 不需要获取全局锁, 效率高 - 工作线程的复用: 执行完一个任务, 不断从
任务队列取任务, 避免因创建和销毁操作系统线程带来的性能消耗 - 获取全局锁, 是性能性能瓶颈,
corePoolSize是预热
工作线程的销毁
满足下面条件会销毁:
- 任务队列里没有
job并且工作线程worker数量超过了核心工作线程数corePoolSize. - 任务队列里没有
job并且允许工作线程数量小于核心工作线程参数为true, 此场景会至少保留一个工作线程线程.
工作线程空闲后, 最长等待keepAliveTime
ThreadPoolExecutor
1 | ThreadPoolExecutor |
用给定的初始参数和默认的线程工厂及饱和策略创建新的 ThreadPoolExecutor.
使用 Executors 工厂方法之一比使用此通用构造方法方便得多, 但是阿里巴巴Java规约不推荐使用, 见下文
- 参数:
corePoolSize- 池中所保存的线程数, 包括空闲线程.maximumPoolSize- 池中允许的最大线程数.keepAliveTime- 当线程数大于核心时, 此为终止前多余的空闲线程等待新任务的最长时间.unit-keepAliveTime参数的时间单位.workQueue- 执行前用于保持任务的队列. 此队列仅保持由execute方法提交的Runnable任务.
- 抛出:
IllegalArgumentException- 如果corePoolSize或keepAliveTime小于零, 或者maximumPoolSize小于或等于零, 或者corePoolSize大于maximumPoolSize.NullPointerException- 如果workQueue为null
工作线程的存储
使用了HashSet来存储工作线程worker, 通过可重入锁ReentrantLock对其进行并发保护. 每个worker都是一个Runnable接口.
1 | /** |
任务队列 runnableTaskQueue
线程池中的队列采用的是BlockingQueue
ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列, 此队列按FIFO(先进先出)原则对元素进行排序.LinkedBlockingQueue:一个基于链表结构的阻塞队列, 此队列按FIFO排序元素, 吞吐量通常要高于ArrayBlockingQueue. 静态工厂方法Executors.newFixedThreadPool()使用了这个队列.SynchronousQueue:一个不存储元素的阻塞队列. 每个插入操作必须等到另一个线程调用移除操作, 否则插入操作一直处于阻塞状态, 吞吐量通常要高于Linked-BlockingQueue, 静态工厂方法Executors.newCachedThreadPool使用了这个队列.PriorityBlockingQueue:一个具有优先级的无限阻塞队列.
线程工厂 ThreadFactory
设置创建线程的工厂, 通过线程工厂给每个创建出来的线程设置更有意义的名字. 使用开源框架guava提供的ThreadFactoryBuilder可以快速给线程池里的线程设置有意义的名字, 代码如下.
1 | newThreadFactoryBuilder().setNameFormat("XX-task-%d").build(); |
RejectedExecutionHandler(饱和策略)
当队列和线程池都满了, 说明线程池处于饱和状态, 那么必须采取一种策略处理提交的新任务. 这个策略默认情况下是 AbortPolicy , 表示无法 处理新任务时抛出异常. 在JDK 1.5中Java线程池框架提供了以下4种策略.
AbortPolicy:直接抛出异常.CallerRunsPolicy:只用调用者所在线程来运行任务.DiscardOldestPolicy:丢弃队列里最近的一个任务, 并执行当前任务.DiscardPolicy:不处理, 丢弃掉.
当然, 也可以根据应用场景需要来实现RejectedExecutionHandler接口自定义策略. 如记录日志或持久化存储不能处理的任务.
1 | /** |
提交任务: submit与execute
execute 没有返回值
submit 可以通过 Future 接口, 获取任务执行的结果: 线程池会返回一个future类型的对象, 通过这个 future对象可以判断任务是否执行成功, 并且可以通过future的get()方法来获取返回值, get()方法会阻塞当前线程直到任务完成, 而使用get(long timeout, TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回, 这时候有可能任务没有执行完.
关闭: shutdown与shutdownNow
原理是遍历线程池中的工作线程, 然后逐个调用线程的interrupt方法来中断线程, 所以无法响应中断的任务可能永远无法终止.
shutdownNow首先将线程池的状态设置成STOP, 然后尝试停止所有的正在执行或暂停任务的线程, 并返回等待执行任务的列表shutdown只是将线程池的状态设置成SHUTDOWN状态, 然后中断所有没有正在执行任务的线程
获取线程的关闭状态:
调用两个关闭方法, isShutdown方法就会返回true. 当所有的任务都已关闭后, 才表示线程池关闭成功, 这时调用isTerminaed方法会返回true.
至于应该调用哪一种方法来关闭线程池, 应该由提交到线程池的任务特性决定, 通常调用shutdown方法来关闭线程池, 如果任务不一定要执行完, 则可以调用shutdownNow方法.
优化配置
性质不同的任务可以用不同规模的线程池分开处理:
CPU密集型任务应配置尽可能小的线程, 如配置Ncpu+1个线程的线程池IO密集型任务线程并不是一直在执行任务, 则应配置尽可能多的线程, 如2*Ncpu- 混合型的任务, 如果可以拆分, 将其拆分成一个CPU密集型任务和一个IO密集型任务, 只要这两个任务执行的时间相差不是太大, 那么分解后执行的吞吐量将高于串行执行的吞吐量. 如果这两个任务执行时间相差太大, 则没必要进行分解.
- 可以通过
Runtime.getRuntime().availableProcessors()方法获得当前设备的CPU个数 - 如果任务很多, 并且每个任务执行的时间比较短, 可以调大
keepAliveTime时间, 提高线程的利用率. - 优先级不同的任务可以使用优先级队列
PriorityBlockingQueue来处理. 它可以让优先级高的任务先执行. 优先级低的任务可能永远不能执行. - 建议使用有界队列. 有界队列能增加系统的稳定性和预警能力, 可以根据需要设大一点:如果采用无解队列, 当任务无法处理引起堆积, 系统撑爆, 殃及其他业务
通常基于几个维度进行:待处理工作job数、线程池定义的最大最小工作线程数、工作线程闲置时间.
监控
通过线程池提供的参数进行监控, 在监控线程池的时候可以使用以下属性.
taskCount:线程池需要执行的任务数量.completedTaskCount:线程池在运行过程中已完成的任务数量, 小于或等于taskCount.largestPoolSize:线程池里曾经创建过的最大线程数量. 通过这个数据可以知道线程池是否曾经满过. 如该数值等于线程池的最大大小, 则表示线程池曾经满过.getPoolSize:线程池的线程数量. 如果线程池不销毁的话, 线程池里的线程不会自动销 毁, 所以这个大小只增不减.getActiveCount:获取活动的线程数. 通过扩展线程池进行监控.通过继承线程池来自定义线程池, 重写线程池的
beforeExecute、afterExecute和terminated方法, 可以在任务执行前、执行后和线程池关闭前执行一些代码来进行监控. 例如, 监控任务的平均执行时间、最大执行时间和最小执行时间等. 这几个方法在线程池里是空方法.
1 | protected void beforeExecute(Thread t,Runnable r) { |
sample
1 | public class MyScheduledExecutorService extends ScheduledThreadPoolExecutor { |
TODO 等待关闭
Alibaba Java 规约
线程池不允许使用Executors去创建, 而是通过ThreadPoolExecutor的方式, 这样的处理方式让写的同学更加明确线程池的运行规则, 规避资源耗尽的风险. 说明:Executors各个方法的弊端:
1)newFixedThreadPool和newSingleThreadExecutor:
主要问题是堆积的请求处理队列可能会耗费非常大的内存, 甚至OOM.
2)newCachedThreadPool和newScheduledThreadPool:
主要问题是线程数最大数是Integer.MAX_VALUE, 可能会创建数量非常多的线程, 甚至OOM.
Positive example 1:
1 | //org.apache.commons.lang3.concurrent.BasicThreadFactory |
Positive example 2:
1 | ThreadFactory namedThreadFactory = new ThreadFactoryBuilder() |
Positive example 3:
1 | <bean id="userThreadPool" |
1 | //in code |
线程池的关闭
等待关闭超时
next…TODO
- Future接口
- 任务队列的实现
ArrayBlockingQueueLinkedBlockingQueueSynchronousQueuePriorityBlockingQueue- 线程池的底层实现
- guava与线程工厂的使用
- 线程关闭与中断: 强制关闭与响应中断
- Java获取设备参数的API
- 连接池