书接上回:【高频面试点】-【线程池工作流程】-【其一】工作线程数workerCount、线程池运行状态runState的设计及维护
1 前言
在线程池运行状态下,不断往里添加任务时,其内部工作机理相信各位小伙伴都已耳熟能详,即便没有研究过线程池内部实现原理,没有看过源码的同学,也应该像我前文提到的“那个朋友”一样,背八股文怎么也得背熟了。
可是,当你面试时,面试官满足的远远不止于能把主流程背出来,他往往会挑一些刁钻的角度,让毫无准备的你当场懵逼。
比如:
- 当你设置了核心线程数corePoolSize后,创建线程池时会立刻将这些线程预热创建出来吗?
- 我们都知道核心线程是不会被回收的,那线程池是如何实现让核心线程在没有任务时等待不回收呢?
- 线程池是如何“标记”核心线程的呢?如何在没有活儿干的时候让核心线程“待岗”,而非核心线程“直接走人”呢
像极了自有员工和外包员工
2 核心线程会在线程池创建之初就创建吗?
如果你够聪明,即使你不知道答案,光看这个问题,你也能立刻回答:否。
因为这很明显,看似一个疑问句,实则充满了反问句的意味。答案自然是“否”,在解释为什么要这么设计之前,先来看看线程池的源码,理解一下“真相”。
2.1 核心线程随任务添加时创建源码分析
先看看任务添加的入口方法:execute
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
/*
* Proceed in 3 steps:
*
* 1. If fewer than corePoolSize threads are running, try to
* start a new thread with the given command as its first
* task. The call to addWorker atomically checks runState and
* workerCount, and so prevents false alarms that would add
* threads when it shouldn't, by returning false.
*
* 2. If a task can be successfully queued, then we still need
* to double-check whether we should have added a thread
* (because existing ones died since last checking) or that
* the pool shut down since entry into this method. So we
* recheck state and if necessary roll back the enqueuing if
* stopped, or start a new thread if there are none.
*
* 3. If we cannot queue task, then we try to add a new
* thread. If it fails, we know we are shut down or saturated
* and so reject the task.
*/
int c = ctl.get();
// 线程池刚创建,workerCount自然是0,如果设置了corePoolSize > 0,该条件成立
if (workerCountOf(c) < corePoolSize) {
// addWorker方法就是添加任务,第二个参数为true,表示用核心线程执行任务
if (addWorker(command, true))
return;
c = ctl.get();
}
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
else if (!addWorker(command, false))
reject(command);
}
可以看到源码中中文注释的部分,是我添加的释义。接下来关注addWorker方法
private boolean addWorker(Runnable firstTask, boolean core) {
retry:
// 存在并发场景,通过自旋来解决并发安全及重试问题
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);
// Check if queue empty only if necessary.
// 这里是在判断线程池runState状态以及内存队列,如果状态异常,会直接返回false
if (rs >= SHUTDOWN &&
! (rs == SHUTDOWN &&
firstTask == null &&
! workQueue.isEmpty()))
return false;
// 这里则是校验任务数量,判断正在执行的任务是否超过了设置的线程数
for (;;) {
int wc = workerCountOf(c);
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maximumPoolSize))
return false;
if (compareAndIncrementWorkerCount(c))
break retry;
c = ctl.get(); // Re-read ctl
if (runStateOf(c) != rs)
continue retry;
// else CAS failed due to workerCount change; retry inner loop
}
}
boolean workerStarted = false;
boolean workerAdded = false;
Worker w = null;
try {
// 走到这里,说明可以进行任务添加,则包装一个Worker对象
w = new Worker(firstTask);
final Thread t = w.thread;
if (t != null) {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
// Recheck while holding lock.
// Back out on ThreadFactory failure or if
// shut down before lock acquired.
int rs = runStateOf(ctl.get());
// 再次校验线程池状态,相当于一个double check lock
if (rs < SHUTDOWN ||
(rs == SHUTDOWN && firstTask == null)) {
if (t.isAlive()) // precheck that t is startable
throw new IllegalThreadStateException();
workers.add(w);
int s = workers.size();
if (s > largestPoolSize)
largestPoolSize = s;
workerAdded = true;
}
} finally {
mainLock.unlock();
}
// 终于一切顺利,完成任务添加后,直接开启worker线程,开始执行任务
if (workerAdded) {
t.start();
workerStarted = true;
}
}
} finally {
if (! workerStarted)
addWorkerFailed(w);
}
return workerStarted;
}
关注上述源码中我添加的中文注释,代表了任务添加的几个关键步骤。结合最开始的execute方法即可得出结论:线程池刚开始的确没有预热初始化核心线程,是在addWorker方法添加任务的时候,通过Worker包装,并调用Thread#start()方法完成线程创建的。
2.2 线程池为什么不在刚开始预热创建所有线程呢?
要回答这个问题,我可以很负责任的说,没有什么特别的原因,仅仅只是源码这样实现的而已,不要试图去猜测Doug Lea老爷子为什么这样写,有什么特殊的考虑,就像你不要去猜测“XX作者为啥要写窗外的倾盆大雨一样”,既不是为了衬托作者悲伤的心情,也不是为了表达哀怨的气氛,就只是此时正在下倾盆大雨一样。
但如果硬要说,依我的理解,我个人认为这种“懒加载”的方式可能更好,更能够节省CPU和内存资源。有的同学可能会说了,一开始预热好不是更好吗?创建线程开销这么大,高并发时造成任务延迟怎么办?
这种问题完全不必担心,因为如果你非要预热,那你就自行预热好了,你只需要在创建线程池后,调用execute方法添加一个空任务即可,就像这样:
ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 5, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>());;
public void preHot() {
executor.execute(() ->{ });
}
对没错,这样就行了。
然后你试着想想,如果Doug Lea将预热的逻辑写死了,而有的同学又不想要预热,或者说,有的同学代码写的稍微差点,又或者说,生产环境上核心线程数配置设置的很大很大。就没有一点儿回旋的余地,万一不小心将线程数设置的很大,岂不是开局即崩溃,资源直接干满。而这样写呢,相当于把“预热”的权利给了在座的各位自行选择。
3 线程池如何区分“正式员工”和“外包员工”,并让“正式员工”不下岗,“外包员工”用完即辞退呢?
同学们都知道,线程池除了设置“核心线程数”,还能设置“最大线程数”,当后者数量大于前者,并且运行时核心线程数已满、阻塞队列已满,但此时仍有任务添加进来时,会开启“临时线程”进行处理。
这听起来,就像是我们熟悉的正式员工和外包员工的区别。
3.1 寻找源码中是如何进行区分的
还是要回到梦开始的地方,也就是execute函数,提交任务的入口方法
参考本文【2.1 核心线程随任务添加时创建源码分析】
addWorker函数即为提交任务的入口,但它有2个入参,源码注释是这样写的:
/* @param firstTask the task the new thread should run first (or
* null if none). Workers are created with an initial first task
* (in method execute()) to bypass queuing when there are fewer
* than corePoolSize threads (in which case we always start one),
* or when the queue is full (in which case we must bypass queue).
* Initially idle threads are usually created via
* prestartCoreThread or to replace other dying workers.
*
* @param core if true use corePoolSize as bound, else
* maximumPoolSize. (A boolean indicator is used here rather than a
* value to ensure reads of fresh values after checking other pool
* state).
* @return true if successful
*/
private boolean addWorker(Runnable firstTask, boolean core)
core这个boolean类型的参数就决定了是否使用核心线程去执行任务
但,真相真的是这样吗?
3.2 线程池中的“人人机会均等”
再回头看一眼【2.1 核心线程随任务添加时创建源码分析】中的addWorker源码,你会发现,针对core这个入参,全函数就一个地方进行了使用,就是在判断workerCount的时候:
for (;;) {
int wc = workerCountOf(c);
if (wc >= CAPACITY ||
// 这里在使用,wc是workerCount(当前任务数),如果core为true则跟核心线程数比较,否则与最大线程数比较
wc >= (core ? corePoolSize : maximumPoolSize))
return false;
if (compareAndIncrementWorkerCount(c))
break retry;
c = ctl.get(); // Re-read ctl
if (runStateOf(c) != rs)
continue retry;
// else CAS failed due to workerCount change; retry inner loop
}
除此之外,再无其它使用之处,就连下方真正去构建Worker对象,开启线程,也没有将core参数传递进去,更没有标记哪个线程是核心线程。
啊这?!有些不合理呀,既然定义了核心线程,不标记的话我怎么知道应该释放哪一个,应该保留哪一个呢?
这时候不妨坐下来仔细想想,我真的需要去标记核心和非核心线程吗?线程资源与人力资源不同,人力资源都是独立的个体,每个人都是不一样的,学历、经历、智商...,所以才有正式员工与外包员工之分。但线程,作为操作系统的一种资源,它没有区别,至少在线程池看来,视所有线程都是平等的个体,因此,秉承着“谁能干谁干,谁干不了退出”的原则。
在线程池内部,只对线程数量进行管理,每次有线程干完活儿后,看看当前还有多少线程在干活儿,跟我预先设定的名额差多少,多了,您就走,少了或刚刚好。就留下等活儿,就这么简单。
上面的图例简单描述了这个逻辑关系和设计理念,假设了一个场景,设置的核心线程数=2,最大线程数=3,并且当前有3个线程在干活。当然,这是一个比较粗略的示意图,只是为了描述清楚线程池内部“人人平等”的设计理念。
3.3 核心线程不回收的原理探秘
既然没有对核心和非核心线程进行标记,那线程池内部又是如何对核心线程进行阻塞不释放,而对非核心线程在没有任务的时候释放的呢?废话不多说,直接上源码:
首先是runWorker函数,也就是Worker包装类对象实现的Runnable的run方法所调用的函数:
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;
w.firstTask = null;
w.unlock(); // allow interrupts
boolean completedAbruptly = true;
try {
// 获取任务执行的逻辑,当执行完毕后,会调用getTask方法从队列中继续获取任务执行
while (task != null || (task = getTask()) != null) {
w.lock();
// If pool is stopping, ensure thread is interrupted;
// if not, ensure thread is not interrupted. This
// requires a recheck in second case to deal with
// shutdownNow race while clearing interrupt
if ((runStateAtLeast(ctl.get(), STOP) ||
(Thread.interrupted() &&
runStateAtLeast(ctl.get(), STOP))) &&
!wt.isInterrupted())
wt.interrupt();
try {
beforeExecute(wt, task);
Throwable thrown = null;
try {
// 执行真正的业务逻辑
task.run();
} catch (RuntimeException x) {
thrown = x; throw x;
} catch (Error x) {
thrown = x; throw x;
} catch (Throwable x) {
thrown = x; throw new Error(x);
} finally {
afterExecute(task, thrown);
}
} finally {
task = null;
w.completedTasks++;
w.unlock();
}
}
completedAbruptly = false;
} finally {
// 非核心线程超时从阻塞队列中没有获取到任务后,会跳出while循环,走到这里,workerCount - 1
processWorkerExit(w, completedAbruptly);
}
}
关注源码中中文注释部分,该方法是执行业务逻辑的真正入口,当执行完毕后,会通过getTask方法从阻塞队列中获取下一个要执行的任务:
private Runnable getTask() {
boolean timedOut = false; // Did the last poll() time out?
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);
// Check if queue empty only if necessary.
if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
decrementWorkerCount();
return null;
}
int wc = workerCountOf(c);
// Are workers subject to culling?
// 关注这里,一般来说不手动设置,allowCoreThreadTimeOut参数为false,即不允许核心线程超时,因此会走到后面的逻辑判断,判断此时工作线程数是否大于核心线程数
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
if ((wc > maximumPoolSize || (timed && timedOut))
&& (wc > 1 || workQueue.isEmpty())) {
if (compareAndDecrementWorkerCount(c))
return null;
continue;
}
try {
/**
* 如果此时工作线程数是否大于核心线程数,则该线程被视为非核心线程(外包员工),
* 继续压榨一段时间(超时获取一下任务,基于poll方法等一下活儿,还有活儿就获取到任务后
* 返回接着干,否则外层函数会跳出while循环,接着方法栈弹出,线程被释放),
* 否则该线程被视为核心线程(正式员工),不解雇,基于take方法阻塞一直等到活儿来
*/
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();
if (r != null)
return r;
timedOut = true;
} catch (InterruptedException retry) {
timedOut = false;
}
}
}
还是关注源码中我中文注释的部分。
所以看到了吗,控制核心线程一直不释放,非核心线程继续压榨一段时间后再释放的核心逻辑,就在于七大线程池参数之一的阻塞队列,利用阻塞队列的poll和take函数完美实现了该特性,妙哉!
4 所以再回头看看最初的问题,线程池有标记“核心”和“非核心”吗
自然是没有的,在线程池里真正做到了“人人平等”,并且实现了资本主义的精髓,来人即用,用完即走。如果平时活儿不多,现有正式员工慢慢干,活儿一多了就赶紧找外包干,干完走之前还要让你等一会儿,如果此时有活儿来了,不好意思先别着急走,干完了再说。
欲哭无泪,果然程序都来自于生活!
5 写在最后
线程池作为大家最常用的JDK工具之一,不但要知其然还要知其所以然,才能更好的帮助我们在工作中辅助业务实现,避免踩坑,提前预判,合理设置参数。
研究线程池也能帮助我们理解生活的不易,从而热爱生活,理解人生的真谛:没有谁一定是谁的谁,只有你自己,活在当下,享受当下,线程的执行就像一趟火车,终点并不重要,因为谁都能run完任务,只有沿途的风景才是最值得体会的,不是吗?打工人们