文章目录
1.前言
对于JDK的线程池而言,若想要了解线程池的工作状态,了解其成员属性ctl
是必不可少的,其主要包含了两个信息:
- 线程池状态;
- 线程池工作线程数量。
本篇文章将会介绍ctl
属性是如何表达上面的两个信息及线程池如何根据这个成员属性区分执行核心线程和非核心线程。
2.ctl
属性
2.1 状态定义
以下是JDK源码中各个状态的代码定义:
// 运行状态,二进制值:111 0 0000 0000 0000 0000 0000 0000 0000
private static final int RUNNING = -1 << COUNT_BITS;
// 关闭状态,二进制值:000 0 0000 0000 0000 0000 0000 0000 0000
private static final int SHUTDOWN = 0 << COUNT_BITS;
// 停止状态,二进制值:001 0 0000 0000 0000 0000 0000 0000 0000
private static final int STOP = 1 << COUNT_BITS;
// 整理状态,二进制值:010 0 0000 0000 0000 0000 0000 0000 0000
private static final int TIDYING = 2 << COUNT_BITS;
// 终止状态,二进制值:011 0 0000 0000 0000 0000 0000 0000 0000
private static final int TERMINATED = 3 << COUNT_BITS;
先描述下JDK官方对这五个状态的定义:
- RUNNING:可接收新的任务和执行队列任务;
- SHUTDOWN:不再接收新任务,但会执行队列任务;
- STOP:不接收新任务和执行队列任务并且暂定执行中的任务;
- TIDYING:所有任务被终止、工作线程数量为0,线程转变为
TIDYING
状态时将会执行terminated()
钩子方法,可以理解成过渡状态; - TERMINATED:已终止,terminated()方法执行成功后变成终止状态。
2.2 ctl
范围值
废话少说,直接上ctl
属性的范围值图:
JDK线程池限制了容量为2^29-1
,这个容量代表每个状态的范围只有536870911
,因此导致integer被分成了2^3
份这种现象。
2.3 问题及思考
2.3.1 容量的由来
为什么JDK要把一个完整的32位硬生生分成8份?并且还有3/8废弃不使用?个人猜想的是由于JDK线程池定义了5个状态,且开发人员想秀花的把状态定义和工作线程数量合成一个成员属性,使用二进制来区分,那么5个状态就必然会占用3个二进制位。毕竟2个二进制位只能表示2^2
个状态。
因此导致了JDK使用ctl
属性的二进制前三位来表示运行状态,也就占了3个二进制位,导致 32 位的int型只有 29 位来表示线程池工作容量。因此JDK的工作线程数量才为2^29 - 1
。
但即使有这样的缺点也无伤大雅,毕竟有哪台机器可以承受住超过2^29-1
个线程的毒打呢?
2.3.2 RUNNING状态负值的好处
既然因为炫技而导致int类型值的8等分,那么有什么方式可以补救,让状态值相对而言便于理解及工作线程相对而言便于操作?
以下是JDK线程池对于ctl
属性的初始化源码:
// ctl初始值,等于RUNNING状态值=-536870912
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
个人猜想将RUNNING
状态分配到负数范围,一是便于理解线程池状态,只要ctl的值为负数,则说明线程池是正常运行的, 很直观,而如果大于0则需要判断处于哪个区间来确定线程池的具体状态;二是正常的新增线程时,只需要使用AtomicInteger.getAndIncrement()
方法。且当ctl
值增长到0时,就代表进入到了SHUTDOWN
状态,SHUTDOWN
的定义就是不会再允许新增线程任务。判断无法新增线程任务只需要判断状态>=SHUTDOWN
即可实现,使用起来较为方便和自然。
3.线程池关键方法流程
再接着往下看需要抱有几个问题:
- 什么时候才会执行非核心线程?不想看?直接跳到答案处
- JDK线程池具体是如何区分核心线程和非核心线程?不想看?直接跳到答案处
- 核心线程真的可以一直存活吗?不想看?直接跳到答案处
- 非核心线程又为什么只能存在一段时间?不想看?直接跳到答案处
- 非核心线程的存活时间由什么控制?不想看?直接跳到答案处
对需要学习的框架或实现原理最好的方式便是抱有疑问主动的摸索,并且将其进行分块,最后再把其串起来,才能做到深入浅出。
3.1 execute()
执行线程
这个属于是JDK线程池对外开放的最基础核心的方法了,看线程池最基础的接口便能得知:
public interface Executor {
void execute(Runnable command);
}
先看看execute()
的官方源代码:
public void execute(Runnable command) {
// 空指针校验
if (command == null)
throw new NullPointerException();
// 获取ctl值留在本地线程
int c = ctl.get();
// 校验工作线程是否小于核心线程池数量
if (workerCountOf(c) < corePoolSize) {
// 如果小于则添加核心线程,添加成功则直接退出
if (addWorker(command, true))
return;
// 添加失败则再次获取ctl值,因为执行addWorker()时ctl值可能发生了变化
c = ctl.get();
}
// 执行到这里有两种情况:
// 1.添加核心线程失败(有可能是添加核心线程时被其他线程添加了)
// 2.核心线程数量已满
// JDK线程池的逻辑是假设正常状态且核心线程池已满,
// 则会把待运行的任务添加到workQueue中,让工作线程来消费
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
// 再次校验执行状态,如果非RUNNING状态,则删除任务
// 执行拒绝策略
if (! isRunning(recheck) && remove(command))
reject(command);
// 如果处于RUNNING状态,且由于不知名原因工作线程全部终止
// 则添加一个无任务的工作线程
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
// 执行到这里也有两种情况:
// 1.线程池非RUNING状态
// 2.workQueue工作队列已满,需要使用非核心线程来消费
// 如果RUNNING状态添加非核心线程失败又存在两种情况:
// 1.工作线程大于2^29-1容量
// 2.工作线程大于maximumPoolSize成员属性
else if (!addWorker(command, false))
reject(command);
}
其实这个最基础的方法内部执行流程比较简单,以下为方法执行流程图:
结合流程图和源代码可以得知问题 什么时候才会执行非核心线程? 的条件就是:
- 线程池为
RUNNING
状态; - 核心线程数量已满;
- 添加到工作队列
workQueue
失败; - 工作线程数量小于线程最大容量
2^29-1
和maximumPoolSize
成员属性。
上述流程只是展示execute()
的调用逻辑,其中reject()
和addWorker()
方法没有深入,将其逻辑分析分开了。这样一看execute()
的逻辑就一目了然了。对于addWorker()
的一些坑源码注释块也写了。
3.2 addWorker()
添加工作线程
把addWorker()
和其它的方法剥离开,其实这个方法的流程也比较简单,可以分为两段:
- 第一段为判断线程池状态和容量是否满足;
- 第二段为添加工作线程。
以下为第一段逻辑源代码:
private boolean addWorker(Runnable firstTask, boolean core) {
// JDK源码就很喜欢使用代码点这种方式,非常不利于理解
// 本段逻辑总共分两个部分:
// 1.判断线程池状态是否满足添加工作线程条件
// 2.判断线程池容量是否可以添加和使用CAS增加工作线程数量
retry:
for (;;) {
// 首先获取ctl的值
int c = ctl.get();
// 再从ctl值中获取运行状态
int rs = runStateOf(c);
// 当运行状态不为RUNNING时且不满足[复杂条件]时添加失败
// 复杂条件:(SHUTDOWN状态,task为空且工作队列不为空)
// 这个复杂条件的用意就是为了让SHUTDOWN状态至少有工作线程
// 来执行完workQueue队列中的任务
if (rs >= SHUTDOWN &&
! (rs == SHUTDOWN &&
firstTask == null &&
! workQueue.isEmpty()))
return false;
// 看起来很长,但无非就是使用CAS机制来添加工作线程数量
for (;;) {
// 获取线程工作数量
int wc = workerCountOf(c);
// 如果线程工作数量大于了最大容量2^29-1
// 或者添加核心线程时大于了corePoolSize参数
// 或者添加非核心线程大于了maximumPoolSize参数
// 则添加失败
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maximumPoolSize))
return false;
// 使用CAS添加工作线程数量
if (compareAndIncrementWorkerCount(c))
break retry;
// 读取新的ctl值
c = ctl.get();
// 再判断状态是否一致,不一致则说明出现了问题
// 需要重新开始判断状态、容量流程
if (runStateOf(c) != rs)
continue retry;
}
}
// 以下为第二段逻辑源代码...
}
这里有个最坑人的地方就是core
参数,很多人会以为这个参数为true
那就是代表核心线程,false
就是非核心线程。其实非也,这个参数的作用就是判断线程总容量时到底是使用corePoolSize
属性还是maximumPoolSize
属性。因此这里并不是我们第二个问题的答案,点击查看第二个问题。
那接着完成addWorker()
方法的后半段逻辑,这段代码的功能就是实例化添加Worker
工作线程,源代码如下:
private boolean addWorker(Runnable firstTask, boolean core) {
// 以上为第一段逻辑源代码...
// 这段代码核心就一个功能:添加Worker工作线程
boolean workerStarted = false;
boolean workerAdded = false;
Worker w = null;
try {
// 将任务作为构造参数实例化Worker对象
w = new Worker(firstTask);
final Thread t = w.thread;
if (t != null) {
// 由于保存Worker对象的集合是HashSet,因此这里需要加锁
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
// 获取ctl值中的线程池状态,为了防止ThreadFactory出现故障
// 或在获取锁之前线程池关闭退出
int rs = runStateOf(ctl.get());
// 校验线程池状态,满足下列条件即可添加:
// 1.为RUNNING状态
// 2.为SHUTDOWN状态且任务为空,及SHUTDOWN状态添加空的Worker对象
if (rs < SHUTDOWN ||
(rs == SHUTDOWN && firstTask == null)) {
// 线程不能已经运行
if (t.isAlive())
throw new IllegalThreadStateException();
// 将Worker对象添加到HashSet集合workers中
workers.add(w);
int s = workers.size();
// 更新最大池数量
if (s > largestPoolSize)
largestPoolSize = s;
workerAdded = true;
}
} finally {
// 解锁
mainLock.unlock();
}
// 如果添加到集合成功,则启动工作线程,并更新返回值
if (workerAdded) {
// 需要注意,这里的t线程对象运行实际上运行的就是Worker线程
t.start();
workerStarted = true;
}
}
} finally {
// 如果工作线程启动失败,则回滚操作
if (! workerStarted)
addWorkerFailed(w);
}
return workerStarted;
}
上述代码看起来可能很唬人,但实际上核心的流程就 中间判断线程池状态并添加Worker对象 那一段。该方法的流程图如下:
看代码貌似觉得东西很多很杂,但实际上从流程图上看,除了循环多了一点,整个流程是非常简单的。
3.3 Worker
线程
线程池维护的线程便是Worker
线程对象,而Worker
线程对象再去执行外部传入进来需要执行的线程对象,其中最关键的便是runWorker()
和getTask()
两个方法,一个是Worker
线程对象的执行方法,一个是从工作队列中获取任务线程的方法,其中最本质区分非核心线程和核心线程的逻辑便是在getTask()
中完成的。a
3.3.1 runWorker()
入口方法
在介绍最核心的getTask()
方法前先来点小菜,看下runWorker()
方法和getTask()
方法的关系,其源代码如下:
final void runWorker(Worker w) {
// 获取当前线程对象,用于后续判断线程是否执行中
Thread wt = Thread.currentThread();
// 如果不是添加到workQueue队列中,firstTask就是传入的线程任务对象
Runnable task = w.firstTask;
w.firstTask = null;
w.unlock();
boolean completedAbruptly = true;
try {
// 如果传入的线程任务对象不为空则先使用现有的任务对象
// ***否则调用getTask()方法从workQueue中获取任务对象***
// ***这个判断才是这个方法中最核心的***
while (task != null || (task = getTask()) != null) {
w.lock();
// 如果线程池状态不是RUNNING和SHUTDOWN,则不再允许执行
// 任何线程任务对象
if ((runStateAtLeast(ctl.get(), STOP) ||
(Thread.interrupted() &&
runStateAtLeast(ctl.get(), STOP))) &&
!wt.isInterrupted())
wt.interrupt();
try {
// 如果线程池状态为RUNNING或SHUTDOWN则执行任务线程
beforeExecute(wt, task);
Throwable thrown = null;
try {
// 实际执行方法
task.run();
}
// catch块忽略
finally {
afterExecute(task, thrown);
}
// 后面的可以忽略了
} finally {
task = null;
w.completedTasks++;
w.unlock();
}
}
completedAbruptly = false;
} finally {
// 执行到这里说明核心线程已死亡或者非核心线程已经执行完了
// 任务线程,会将本Worker对象进行删除并尝试终止线程池
processWorkerExit(w, completedAbruptly);
}
}
这个流程其实没什么说的,需要注意的点就是while
条件的变更,当getTask()
或任务线程为空的时候,Worker就会消亡,因此在实际的线程运行过程中,getTask()
就是最重要的,毕竟firstTask最多只会循环一次。
3.3.2 getTask()
获取任务线程
获取看到这里有人都忘了前面留有的几个问题了,(点击跳转到问题列表)一共五个问题,接下来的四个问题将全部在这里方法流程中揭晓。在看源码前有个很重要的点需要注意——如果getTask()
方法返回null,那么Worker对象将会消亡。 其源码流程比较简单,先看源码:
private Runnable getTask() {
// 用来标记从workQueue队列中获取任务线程是否超时
boolean timedOut = false;
for (;;) {
// 获取ctl值并获取线程池状态
int c = ctl.get();
int rs = runStateOf(c);
// 碰到以下两种情况将返回null
// 1.线程池状态非RUNNING状态或SHUTDOWN状态
// 2.非RUNNING状态且任务队列为空
if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
// 减少工作线程数量,因为本Worker线程对象即将消亡
decrementWorkerCount();
return null;
}
// 执行到这说明线程池为RUNNING状态或SHUTDOWN状态且工作队列对任务
// 获取工作线程数量
int wc = workerCountOf(c);
// allowCoreThreadTimeOut属性决定了线程池核心线程是否也可以消亡
// allowCoreThreadTimeOut默认为false,如果为false,那么timed
// 值就是最本质区分核心线程和非核心线程的参数
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
// 这里的判断逻辑看起来很唬人,拆开来看就很好理解了
// 1.如果工作线程大于maximumPoolSize属性且工作队列为空
// 2.如果非核心线程超时且工作线程数量大于1
// 3.如果非核心线程超时且工作任务队列为空
// 4.如果allowCoreThreadTimeOut为true且核心线程获取任务超时且
// 工作线程至少有一个
// 5.如果allowCoreThreadTimeOut为true且核心线程获取任务超时且
// 工作队列为空
// 如果以上五种情况任意一种满足,且减少工作线程数量成功
// 则Worker线程对象会消亡,不论这个Worker线程对象是核心线程还是非核心线程
if ((wc > maximumPoolSize || (timed && timedOut))
&& (wc > 1 || workQueue.isEmpty())) {
if (compareAndDecrementWorkerCount(c))
return null;
continue;
}
try {
// 说人话就是:
// 非核心线程将会使用在规定时间内获取任务线程对象方法
// 没获取到则返回为null
// 如果allowCoreThreadTimeOut为true,则核心线程和
// 非核心线程一样可能会返回null
// 如果allowCoreThreadTimeOut为false,则核心线程阻塞获取
// 线程任务
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();
// 不为空则返回线程任务对象,不会判断Worker对象获取任务超时
if (r != null)
return r;
// 线程对象为空则获取任务超时
timedOut = true;
} catch (InterruptedException retry) {
// 如果发生了异常不设置为超时
timedOut = false;
}
}
}
看完是不是觉得自己会了但又没完全会?这段代码是需要好好结合runWorker()
方法和线程池状态值去推敲的,现在来一一回答一下剩下来的几个问题:
1.JDK线程池具体是如何区分核心线程和非核心线程?
答:分情况讨论,如果allowCoreThreadTimeOut为true,那么对于线程池而言,核心线程和非核心线程最终的处理是一模一样的,没有差别;allowCoreThreadTimeOut默认为false,则是根据工作线程数量是否大于corePoolSize
属性,大于则为非核心线程,否则为核心线程。
2.核心线程真的可以一直存活吗?
答:如果allowCoreThreadTimeOut为true,则工作队列为空核心线程一直获取不到任务对象时,核心线程也会像非核心线程一样消亡。
3.非核心线程又为什么只能存在一段时间?
答:如果Worker对象被判断为非核心线程,从workQueue工作任务队列中获取任务线程时会有时间限制,如果超出时间还没有获取到任务线程,Worker对象将会消亡;但是allowCoreThreadTimeOut为false核心线程使用的是阻塞获取,将永远不会返回null,因此核心线程这种情况可以一直存在。
4.非核心线程的存活时间由什么控制?
答:由keepAliveTime
属性,非核心线程在这个时间内为获取到任务线程,将会消亡(allowCoreThreadTimeOut为true时核心线程存在的时间也是由这个属性控制的)。
由此可见,网上通常说的核心线程是永远不会死亡的说法是不严谨的,核心线程也有可能会被当成非核心线程一样处理消亡。正所谓JDK源码读一遍就能多收获一份,多多推敲思考。