作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO
联系qq:184480602,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬
梳理完JDK线程池的继承体系,相信大家对线程池的上下关系、发展脉络已经有了初步认识。但实际工作中,Executor、ExecutorService、AbstractExecutorService并不常见,我们一般直接使用ThreadPoolExecutor这个实现类。通常情况下,大家讨论线程池时,其实都是在讨论ThreadPoolExecutor,只不过他们自己都没意识到。
上回说到,ExecutorService接口新增了submit(),可以接收Runnable/Callable,并且支持返回值。而AbstractExecutorService则“初步”实现了submit():
但从源码来看,submit()仅仅做了Runnable/Callable的统一包装,具体的任务执行还是交给Executor#execute()。也就是通过模板方法模式,把具体的实现交给了子类。而这个“子类”,一般就是指ThreadPoolExecutor,本篇文章我们将研究它是如何执行上级指派的任务(Task)的。
开头先给大家留一个问题,请把下面的代码拷贝到本地执行:
@Slf4j
public class ThreadPoolDebug {
public static void main(String[] args) {
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
1, // 核心线程数1
2, // 最大线程数2
1, // 存活时间设为1小时,避免测试期间线程被回收
TimeUnit.HOURS,
new ArrayBlockingQueue<>(1) // 阻塞队列长度为1
);
threadPoolExecutor.execute(() -> {
log.info("{}:执行第1个任务...", Thread.currentThread().getName());
sleep(100);
});
sleep(1);
threadPoolExecutor.execute(() -> {
log.info("{}:执行第2个任务...", Thread.currentThread().getName());
sleep(100);
});
sleep(1);
threadPoolExecutor.execute(() -> {
log.info("{}:执行第3个任务...", Thread.currentThread().getName());
sleep(100);
});
sleep(1);
threadPoolExecutor.execute(() -> {
log.info("{}:执行第4个任务...", Thread.currentThread().getName());
sleep(100);
});
sleep(1);
log.info("main结束");
}
private static void sleep(int seconds) {
try {
TimeUnit.SECONDS.sleep(seconds);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
运行结果为:
我的问题是:第2个任务去哪了呢?后面揭晓。
execute()执行流程
AbstractExecutorService#submit()底层调用了execute(),也就是说主要逻辑都在execute()中:
而ThreadPoolExecutor对execute()做了具体实现:
鄙人虽然日语专业出身,但语言都是相通的,姑且翻译一下:
public void execute(Runnable command) { // 这里的command,就是submit()里封装的FutureTask
if (command == null)
throw new NullPointerException();
/*
* Proceed in 3 steps:
* 分3步处理:
*
* 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.
* 如果当前线程数少于corePoolSize,就新开一个【核心线程】处理当前请求
*
* 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.
* 如果corePoolSize已经满了,就把当前任务【加入任务队列】等待执行
*
* 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.
* 如果任务队列也满了,尝试开启新的【非核心线程】,如果开启失败(超过maximumPoolSize),则执行【拒绝策略】
*
*/
//(1)获取ctl。ctl用来标记线程池状态(高3位),线程个数(低29位)
int c = ctl.get();
//(2)当前线程池线程个数是否小于corePoolSize,小于则【创建核心线程】处理当前任务
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
//(3)如果线程池处于RUNNING状态,则添加任务到阻塞队列(3.x这些分支可以不看,和主流程关系不大,都是兜底操作)
if (isRunning(c) && workQueue.offer(command)) {
//(3.1)二次检查
int recheck = ctl.get();
//(3.2)如果当前线程池状态不是RUNNING则从队列删除任务,并执行拒绝策略
if (! isRunning(recheck) && remove(command))
reject(command);
//(3.3)否者如果当前线程池线程空,则添加一个线程(原则上可以设置所有线程空闲回收,所以可能为空)
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
//(4)第3步失败,说明队列满了,则【新增非核心线程】处理当前任务
else if (!addWorker(command, false))
// (4.1) 非核心线程已达上限,触发拒绝策略
reject(command);
}
execute()本身思路很明确,它安排了任务处理的总流程:
- 优先使用核心线程处理任务
- 核心线程满了,优先入队
- 等到队列满了,尝试开启非核心线程处理任务
- 如果非核心线程也满了,那就没办法了,执行拒绝策略
看到这我们恍然大悟:原来那些年死记硬背的“线程池执行过程”出自execute()内部的注释!
回到开头那个问题:第2个任务去哪了?(注意,所有任务sleep 100秒,可以理解为每个线程无法处理其他任务)
- 第1个任务:当前线程数为0 < corePoolSize,于是创建核心线程处理任务1 【参见(2)】
- 第2个任务:当前线程数为1,不满足 currentSize < corePoolSize,于是任务2进入任务队列 【参见(3)】
- 第3个任务:当前线程数为1,不满足 currentSize < corePoolSize,并且队列也满了,于是创建非核心线程处理当前任务 【参见(4)】
- 第4个任务:当前线程数为2,不满足 currentSize < corePoolSize,并且队列也满了,总线程数也达到上限,触发拒绝策略 【参见(4.1)】
由于任务1和3分别霸占着核心线程和非核心线程,所以任务队列里的任务2没有线程去处理。但实际上,只要你等上100秒,会发现任务2就被处理了:
因为此时任务1和3已经被处理完了,线程又空闲下来了,于是回到任务队列里拿任务。
核心线程是如何处理任务的?
- 优先使用核心线程处理任务
- 核心线程满了,优先入队
- 等到队列满了,尝试开启非核心线程处理任务
- 如果非核心线程也满了,那就没办法了,执行拒绝策略
以上4种情况,除了拒绝策略,核心线程处理任务的流程相对简单,所以我们优先理解核心线程的任务处理流程(其他的都差不多)。
public void execute(Runnable command) { // 这里的command,就是submit()里封装的FutureTask
if (command == null)
throw new NullPointerException();
//(1)获取ctl。ctl用来标记线程池状态(高3位),线程个数(低29位)
int c = ctl.get();
//(2)当前线程池线程个数是否小于corePoolSize,小于则【创建核心线程】处理当前任务。
if (workerCountOf(c) < corePoolSize) {
// addWorker()第二个参数代表是否核心线程
if (addWorker(command, true))
// 注意,核心线程的创建、执行都在addWorker()方法中,一旦任务提交给核心线程,整个execute()就结束了
return;
c = ctl.get();
}
// ...
}
addWorker()的大概流程是:
- 先把task封装成Worker
- 经过一系列步骤,启动Worker里的Thread,线程开启后会执行Worker里的Task
大致流程就是这样,接下来再探究细节。
我们一起来看看Worker是啥:
看上面Worker的构造器,我们不难发现 Worker = Thread + Task:
- getThreadFactory().new Thread()会创建一个线程
- Task则是Executor#execute(task)提交的那个任务
之前在多线程基础篇里我们提到过一个变种用法:
ThreadPoolExecutor中Worker也用了类似的写法:
右图Worker#run()又调用ThreadPoolExecutor#runWorker():
整个调用链是:
注意,Executor#execute()和FutureTask#run()之间不是同步调用,而是通过线程池的异步线程执行的。
也就是说,线程池里的Worker=Thread+Task,当线程启动后,会执行Worker#run(),但Worker并不是真正的任务,所以又要转一层,然后线程最终执行Worker内部的Task,也就是执行我们提交的任务。
线程池的生产消费模型
核心线程的处理逻辑虽然绕,但理清楚以后还是简单的。但从上面的介绍来看,你会发现和普通的new Thread(target).start()没太大区别,不是说线程池本质是生产消费模型吗?ThreadPoolExecutor的生产者是谁,消费者又是谁,什么时候消费、如何消费?
前面提到过,如果往线程池不断提交任务,大致会经历4个阶段:
- 核心线程处理任务
- 任务进入任务队列等待
- 非核心线程处理任务
- 拒绝策略
任务进入队列即为生产,而当核心线程/非核心线程处理完手头的任务并空闲时,就会从workQueue获取任务并处理,这就是ThreadPoolExecutor的生产消费模型。
ThreadPoolExecutor中,线程池是workers,任务队列时workQueue,不要搞混了
阻塞队列
ThreadPoolExecutor的生产消费模型是通过阻塞队列实现的,我们可以在创建ThreadPoolExecutor时指定使用哪种阻塞队列:
比如文章开头Demo中,我们使用了ArrayBlockingQueue。关于阻塞队列,之前篇章已经介绍过了,就不展开了。
生产者
我们往线程池提交任务的过程就是生产的过程:
public void execute(Runnable command) { // 这里的command,就是submit()里封装的FutureTask
if (command == null)
throw new NullPointerException();
int c = ctl.get();
// 假设核心线程数已满,跳过这一步
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
// workQueue.offer(command):尝试把任务加入到workQueue(任务队列),这个workQueue就是new ThreadPoolExecutor()时指定的BlockingQueue
if (isRunning(c) && workQueue.offer(command)) {
// ...
}
else if (!addWorker(command, false))
reject(command);
}
有一点需要特别注意,之前我们模拟阻塞队列时,对于入队、出队两个操作都会进行阻塞(put、take),但ThreadPoolExecutor显然不允许这样做:
调用方提交一个任务给线程池,本来就是为了异步,结果你特么地把人家缠住了(阻塞)...
失败抛异常 | 失败返回特殊值 | 阻塞 | 阻塞(指定超时时间) | |
插入 | add(e) | offer(e) | put(e) | offer(e, time, unit) |
删除 | remove() | poll() | take() | poll(time, unit) |
查询 | element() | peek() | 无 | 无 |
所以ThreadPoolExecutor并没有对入队进行阻塞,而是判断队列长度是否已满,如果满了就返回false,execute()流程继续往下触发拒绝策略。
具体有哪些拒绝策略,大家自行了解。如果必要,我们也可以自定义拒绝策略,并在new ThreadPoolExecutor()时传入。
消费者
核心/非核心线程处理完手头任务后,是如何从任务队列获取新任务的呢?还记得ThreadPoolExecutor#runWorker()吗,异步线程开启后最终会调用它。
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;
// ...
try {
// 线程开启后,进入循环:当前任务不为空 || 队列任务不为空
while (task != null || (task = getTask()) != null) {
// ...
try {
// ...
task.run();
// ...
} finally {
// ...
}
}
completedAbruptly = false;
} finally {
processWorkerExit(w, completedAbruptly);
}
}
整个调用路径是:ThreadPoolExecutor#execute() ==> addWorker() ==> runWorker() ==> getTask()(绿色表示交给子线程执行)
也就是说,addWorker()可不是简单地创建线程并执行当前任务就完事了,runWoker()内部会循环,看看队列里有没有任务需要处理(getTask),是个很热心的小伙子!
线程的复用与销毁
大家有没有想过,一般来说new Thread().start()执行完目标任务后,就会自然销毁。那么线程池是如何做到任务跑完了之后线程不销毁的呢?更准确地说:
核心线程是如何复用的?
上面提到过,runWorker()内部其实是一个循环,不断地从任务队列中获取任务并执行。我们可以去看看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);
// 是否需要检测超时:允许所有线程超时回收(包括核心线程) || 当前线程数超过核心线程
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
// 超时了,跳出循环
if ((wc > maximumPoolSize || (timed && timedOut))
&& (wc > 1 || workQueue.isEmpty())) {
if (compareAndDecrementWorkerCount(c))
return null;
continue;
}
try {
// 是否需要检测超时
// 1.需要:poll阻塞获取,等待keepAliveTime,等待结束就返回,不管有没有获取到任务
// 2.不需要:一直阻塞,直到获取结果
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();
if (r != null)
return r;
// r==null,任务为空,timedOut=true
timedOut = true;
} catch (InterruptedException retry) {
timedOut = false;
}
}
}
复用
如果把上面代码按某一个逻辑分支做简化:
private Runnable getTask() {
boolean timedOut = false; // Did the last poll() time out?
// 尝试循环获取任务
for (;;) {
// 是否需要检测超时:当前线程数超过核心线程
boolean timed = wc > corePoolSize;
// 超时了,return null
if (timed && timedOut) {
return null;
}
try {
// 是否需要检测超时
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();
if (r != null)
return r;
timedOut = true;
} catch (InterruptedException retry) {
timedOut = false;
}
}
}
也就是说,如果是核心线程,timed永远为false,那么就会调用workQueue.take()一直阻塞下去,直到有新的任务提交进来。但是处理结束后,还是会进入循环,周而复始。由于线程永远处于阻塞等待任务、执行任务、继续阻塞等待任务的死循环中,也就永远不会销毁了。
所以线程池之所以能复用线程,仅仅是 keeps threads busy working 罢了。
销毁
部分同学可能会有的疑惑:
- 万一当前执行任务的是核心线程,却被销毁了咋办?
- 怎样才算销毁了线程?
这里特别说明一下:
- 所谓的“核心线程不会被销毁”,并不是指某些特定的线程不会被销毁,而是说无论线程怎么销毁,最终要保证池中活跃线程数不小于corePoolSize!
- 所谓的线程销毁,其实就是让任务继续往下走,执行完了也就结束了,和new Thread().start()是一样的
如果不考虑allowCoreThreadTimeOut(一般不会刻意设置为true):
- 那么当线程数不超过corePoolSize时,每一个线程都是核心线程,此时并不需要进行“超时检测”,所以线程会直接调用BlockingQueue#take()阻塞等待,直到有新的任务被提交。即使跳出getTask(),回到runWorker()执行完新的任务,也别指望线程就这么结束了,因为runWorker()本身也是循环,又会回到getTask()...从宏观上来看,就形成了所谓的“线程池Thread复用”
- 当前线程数超过corePoolSize,那么就会getTask()里的循环就会进行“超时检测”。所谓的超时检测,其实就是“阻塞等待keepAliveTime”,等待结束直接返回,不论是否拿到任务。假设任务为null,就会跳过if判断,设置timeOut=true(线程当前数超过了corePoolSize && 这次又取不到任务,说明啥?线程池没任务,空闲了,所以施主你该走了)
设置完timeOut=true后还要重新回到循环:
private Runnable getTask() {
boolean timedOut = false; // Did the last poll() time out?
// 尝试循环获取任务
for (;;) {
// 省略部分代码...
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
// 此时条件成立
if ((wc > maximumPoolSize || (timed && timedOut))
&& (wc > 1 || workQueue.isEmpty())) {
// 执行成功
if (compareAndDecrementWorkerCount(c))
// 返回空任务,跳出当前循环。而外层runWorker()由于 task==null,也会跳出循环
return null;
continue;
}
try {
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();
if (r != null)
return r;
timedOut = true;
} catch (InterruptedException retry) {
timedOut = false;
}
}
}
也就是说,只有当task=null && 队列为空 && 当前线程数超过corePoolSize,当前线程才可能连续跳过getTask()循环、runWorker()循环,最终执行完任务被销毁(太难了,一个线程想死却死不了)...
如何验证上面的结论呢?
@Slf4j
public class ThreadPoolDebug {
public static void main(String[] args) {
// 核心线程1,最大线程2,提交4个任务:第1个任务交给核心线程、第2个任务入队、第3个任务交给非核心线程、第4个任务被拒绝
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
1,
2,
1,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(1)
);
threadPoolExecutor.execute(() -> {
log.info("{}:执行第1个任务...", Thread.currentThread().getName());
sleep(3);
});
threadPoolExecutor.execute(() -> {
log.info("{}:执行第2个任务...", Thread.currentThread().getName());
sleep(3);
});
threadPoolExecutor.execute(() -> {
log.info("{}:执行第3个任务...", Thread.currentThread().getName());
sleep(3);
});
threadPoolExecutor.execute(() -> {
log.info("{}:执行第4个任务...", Thread.currentThread().getName());
sleep(3);
});
log.info("main结束");
}
private static void sleep(int seconds) {
try {
TimeUnit.SECONDS.sleep(seconds);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
执行完队列中的第2个任务后,return null,任务全部执行完毕,线程run()结束,线程销毁。
小结
如果把FutureTask也算上,实际上有4种执行多线程任务的方式:
- Thread:
-
- 重写Thread的run()
- 通过Thread的构造器传入Runnable实例
- 通过Thread的构造器传入Runnable实例(FutureTask,内部包装了Runnable/Callable)
- 线程池:
-
- 通过线程池(Runnable/Callable都行)
Thread和线程池看起来是单个线程和线程群体的关系,但实际上Thread和线程池还有个连接点:FutureTask。无论是Thread还是线程池,都可以接收FutureTask,只不过Thread使用FutureTask时需要我们在外部自己包装好Runnable和Callable,而线程池把这个操作内化了。
大家如果有兴趣,可以继续扩展上一篇的山寨ThreadPool,让构造器支持拒绝策略和ThreadFactory,这样7大参数都全了。
作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO
进群,大家一起学习,一起进步,一起对抗互联网寒冬