Java线程池ThreadPoolExecutor源码分析
前言
ThreadPoolExecutor是jdk内置线程池的一个实现,基本上大部分情况都会使用这个线程池完成各项操作。
ThreadPoolExecutor的状态和属性
ThreadPoolExecutor线程池有5个状态,分别是:
RUNNING:可以接受新的任务,也可以处理阻塞队列里的任务SHUTDOWN:不接受新的任务,但是可以处理阻塞队列里的任务STOP:不接受新的任务,不处理阻塞队列里的任务,中断正在处理的任务TIDYING:过渡状态,也就是说所有的任务都执行完了,当前线程池已经没有有效的线程,这个时候线程池的状态将会TIDYING,并且将要调用terminated方法TERMINATED:终止状态。terminated方法调用完成以后的状态
状态之间可以进行转换:
RUNNING -> SHUTDOWN:手动调用shutdown方法,或者ThreadPoolExecutor要被GC回收的时候调用finalize方法,finalize方法内部也会调用shutdown方法
(RUNNING or SHUTDOWN) -> STOP:调用shutdownNow方法
SHUTDOWN -> TIDYING:当队列和线程池都为空的时候
STOP -> TIDYING:当线程池为空的时候
TIDYING -> TERMINATED:terminated方法调用完成之后
ThreadPoolExecutor内部还保存着线程池的有效线程个数。
状态和线程数在ThreadPoolExecutor内部使用一个整型变量保存,没错,一个变量表示两种含义。
为什么一个整型变量既可以保存状态,又可以保存数量? 分析一下:
首先,我们知道Java中1个整型占4个字节,也就是32位,所以1个整型有32位。
所以整型1用二进制表示就是:00000000000000000000000000000001
整型-1用二进制表示就是:11111111111111111111111111111111(这个是补码,不懂的同学可以看下原码,反码,补码的知识)
在ThreadPoolExecutor,整型中32位的前3位用来表示线程池状态,后29位表示线程池中有效的线程数。
1
|
// 前3位表示状态,所以线程数占29位
|
线程池容量大小为 1 << 29 - 1 = 00011111111111111111111111111111(二进制),代码如下
1
|
private static final int CAPACITY = (1 << COUNT_BITS) - 1;
|
RUNNING状态 -1 << 29 = 11111111111111111111111111111111 << 29 = 11100000000000000000000000000000(前3位为111):
1
|
private static final int RUNNING = -1 << COUNT_BITS;
|
SHUTDOWN状态 0 << 29 = 00000000000000000000000000000000 << 29 = 00000000000000000000000000000000(前3位为000)
1
|
private static final int SHUTDOWN = 0 << COUNT_BITS;
|
STOP状态 1 << 29 = 00000000000000000000000000000001 << 29 = 00100000000000000000000000000000(前3位为001):
1
|
private static final int STOP = 1 << COUNT_BITS;
|
TIDYING状态 2 << 29 = 00000000000000000000000000000010 << 29 = 01000000000000000000000000000000(前3位为010):
1
|
private static final int TIDYING = 2 << COUNT_BITS;
|
TERMINATED状态 3 << 29 = 00000000000000000000000000000011 << 29 = 01100000000000000000000000000000(前3位为011):
1
|
private static final int TERMINATED = 3 << COUNT_BITS;
|
清楚状态位之后,下面是获得状态和线程数的内部方法:
1
|
// 得到线程数,也就是后29位的数字。 直接跟CAPACITY做一个与操作即可,CAPACITY就是的值就 1 << 29 - 1 = 00011111111111111111111111111111。 与操作的话前面3位肯定为0,相当于直接取后29位的值
|
线程池初始化状态线程数变量:
1
|
// 初始化状态和数量,状态为RUNNING,线程数为0
|
ThreadPoolExecutor执行任务
使用ThreadPoolExecutor执行任务的时候,可以使用execute或submit方法,submit方法如下:
1
|
public Future<?> submit(Runnable task) {
|
很明显地看到,submit方法内部使用了execute方法,而且submit方法是有返回值的。在调用execute方法之前,使用FutureTask包装一个Runnable,这个FutureTask就是返回值。
由于submit方法内部调用execute方法,所以execute方法就是执行任务的方法,来看一下execute方法,execute方法内部分3个步骤进行处理。
- 如果当前正在执行的
Worker数量比corePoolSize(基本大小)要小。直接创建一个新的Worker执行任务,会调用addWorker方法 - 如果当前正在执行的
Worker数量大于等于corePoolSize(基本大小)。将任务放到阻塞队列里,如果阻塞队列没满并且状态是RUNNING的话,直接丢到阻塞队列,否则执行第3步。丢到阻塞队列之后,还需要再做一次验证(丢到阻塞队列之后可能另外一个线程关闭了线程池或者刚刚加入到队列的线程死了)。如果这个时候线程池不在RUNNING状态,把刚刚丢入队列的任务remove掉,调用reject方法,否则查看Worker数量,如果Worker数量为0,起一个新的Worker去阻塞队列里拿任务执行 - 丢到阻塞失败的话,会调用
addWorker方法尝试起一个新的Worker去阻塞队列拿任务并执行任务,如果这个新的Worker创建失败,调用reject方法
上面说的Worker可以暂时理解为一个执行任务的线程。
execute方法源码如下,上面提到的3个步骤对应源码中的3个注释:
1
|
public void execute(Runnable command) {
|
addWorker关系着如何起一个线程,再看addWorker方法之前,先看一下ThreadPoolExecutor的一个内部类Worker, Worker是一个AQS的实现类(为何设计成一个AQS在闲置Worker里会说明),同时也是一个实现Runnable的类,使用独占锁,它的构造函数只接受一个Runnable参数,内部保存着这个Runnable属性,还有一个thread线程属性用于包装这个Runnable(这个thread属性使用ThreadFactory构造,在构造函数内完成thread线程的构造),另外还有一个completedTasks计数器表示这个Worker完成的任务数。Worker类复写了run方法,使用ThreadPoolExecutor的runWorker方法(在addWorker方法里调用),直接启动Worker的话,会调用ThreadPoolExecutor的runWork方法。需要特别注意的是这个Worker是实现了Runnable接口的,thread线程属性使用ThreadFactory构造Thread的时候,构造的Thread中使用的Runnable其实就是Worker。下面的Worker的源码:
1
|
private final class Worker
|
接下来看一下addWorker源码:
1
|
// 两个参数,firstTask表示需要跑的任务。boolean类型的core参数为true的话表示使用线程池的基本大小,为false使用线程池最大大小
|
Worker中的线程start的时候,调用Worker本身run方法,这个run方法之前分析过,调用外部类ThreadPoolExecutor的runWorker方法,直接看runWorker方法:
1
|
final void runWorker(Worker w) {
|
我们看一下getTask方法是如何获得任务的:
1
|
// 如果发生了以下四件事中的任意一件,那么Worker需要被回收:
|
如果getTask返回的是null,那说明阻塞队列已经没有任务并且当前调用getTask的Worker需要被回收,那么会调用processWorkerExit方法进行回收:
1
|
private void processWorkerExit(Worker w, boolean completedAbruptly) {
|
在回收Worker的时候线程池会尝试结束自己的运行,tryTerminate方法:
1
|
final void tryTerminate() {
|
解释了这么多,对线程池的启动并且执行任务做一个总结:
首先,构造线程池的时候,需要一些参数。
文章中的结尾已经说明了一下重要参数的意义。
线程池构造完毕之后,如果用户调用了execute或者submit方法的时候,最后都会使用execute方法执行。
execute方法内部分3种情况处理任务:
- 如果当前正在执行的
Worker数量比corePoolSize(基本大小)要小。直接创建一个新的Worker执行任务,会调用addWorker方法 - 如果当前正在执行的
Worker数量大于等于corePoolSize(基本大小)。将任务放到阻塞队列里,如果阻塞队列没满并且状态是RUNNING的话,直接丢到阻塞队列,否则执行第3步 - 丢到阻塞失败的话,会调用
addWorker方法尝试起一个新的Worker去阻塞队列拿任务并执行任务,如果这个新的Worker创建失败,调用reject方法
线程池中的这个基本大小指的是Worker的数量。一个Worker是一个Runnable的实现类,会被当做一个线程进行启动。Worker内部带有一个Runnable属性firstTask,这个firstTask可以为null,为null的话Worker会去阻塞队列拿任务执行,否则会先执行这个任务,执行完毕之后再去阻塞队列继续拿任务执行。
所以说如果Worker数量超过了基本大小,那么任务都会在阻塞队列里,当Worker执行完了它的第一个任务之后,就会去阻塞队列里拿其他任务继续执行。
Worker在执行的时候会根据一些参数进行调节,比如Worker数量超过了线程池基本大小或者超时时间到了等因素,这个时候Worker会被线程池回收,线程池会尽量保持内部的Worker数量不超过基本大小。
另外Worker执行任务的时候调用的是Runnable的run方法,而不是start方法,调用了start方法就相当于另外再起一个线程了。
Worker在回收的时候会尝试终止线程池。尝试关闭线程池的时候,会检查是否还有Worker在工作,检查线程池的状态,没问题的话会将状态过度到TIDYING状态,之后调用terminated方法,terminated方法调用完成之后将线程池状态更新到TERMINATED。
ThreadPoolExecutor的关闭
线程池的启动过程分析好了之后,接下来看线程池的关闭操作:
shutdown方法,关闭线程池,关闭之后阻塞队列里的任务不受影响,会继续被Worker处理,但是新的任务不会被接受:
1
|
public void shutdown() {
|
interruptIdleWorkers方法,注意,这个方法打断的是闲置Worker,打断闲置Worker之后,getTask方法会返回null,然后Worker会被回收。那什么是闲置Worker呢?
闲置Worker是这样解释的:
Worker运行的时候会去阻塞队列拿数据(getTask方法),拿的时候如果没有设置超时时间,那么会一直阻塞等待阻塞队列进数据,这样的Worker就被称为闲置Worker。由于Worker也是一个AQS,在runWorker方法里会有一对lock和unlock操作,这对lock操作是为了确保Worker不是一个闲置Worker。
所以Worker被设计成一个AQS是为了根据Worker的锁来判断是否是闲置线程,是否可以被强制中断。
下面我们看下interruptIdleWorkers方法:
1
|
// 调用他的一个重载方法,传入了参数false,表示要中断所有的正在运行的闲置Worker,如果为true表示只打断一个闲置Worker
|
shutdown方法将线程池状态改成SHUTDOWN,线程池还能继续处理阻塞队列里的任务,并且会回收一些闲置的Worker。但是shutdownNow方法不一样,它会把线程池状态改成STOP状态,这样不会处理阻塞队列里的任务,也不会处理新的任务:
1
|
// shutdownNow方法会有返回值的,返回的是一个任务列表,而shutdown方法没有返回值
|
shutdownNow的中断和shutdown方法不一样,调用的是interruptWorkers方法:
1
|
private void interruptWorkers() {
|
Worker的interruptIfStarted方法中断Worker的执行:
1
|
void interruptIfStarted() {
|
线程池关闭总结:
线程池的关闭主要是两个方法,shutdown和shutdownNow方法。
shutdown方法会更新状态到SHUTDOWN,不会影响阻塞队列里任务的执行,但是不会执行新进来的任务。同时也会回收闲置的Worker,闲置Worker的定义上面已经说过了。
shutdownNow方法会更新状态到STOP,会影响阻塞队列的任务执行,也不会执行新进来的任务。同时会回收所有的Worker。
原文作者:等一夏_81f7(简书作者)
原文标题:Java线程池ThreadPoolExecutor源码分析

浙公网安备 33010602011771号