02小节:深度解析线程池底层原理
作者:程序员马丁
热门项目实战社群,收获国内众多知名公司面试青睐,近千名同学面试成功!助力你在校招或社招上拿个offer。
深度解析线程池底层原理,元数据信息:
- 什么是线程池oneThread:https://t.zsxq.com/5GfrN
- 代码仓库:https://gitcode.net/nageoffer/onethread —— 申请项目权限参考上述线程池项目链接
- 章节难度:★★☆☆☆ - 中等
- 视频地址:本章节内容简单,无
©版权所有 - 拿个offer-开源&项目实战星球专属学习项目,依据《中华人民共和国著作权法实施条例》和《知识星球产权保护》,严禁未经本项目原作者明确书面授权擅自分享至 GitHub、Gitee 等任何开放平台。违者将面临法律追究。
内容摘要:线程池通过复用线程、管理任务队列与智能调度策略,大幅提升系统吞吐并降低资源消耗。其核心在于ThreadPoolExecutor的三大组件:工作线程(Worker)实现任务执行与复用,阻塞队列缓冲任务洪峰,位运算变量(ctl)统一管控状态与线程数。这种设计完美平衡了响应速度与资源利用率,是高并发场景的底层基石。本篇文章跟着马哥一起看下线程池底层设计之美。
课程目录如下所示:
- 线程池介绍
- 线程池概要
- 状态控制
- 线程池状态
- 任务调度
- 文末总结
线程池介绍
通常当我们提到“线程池”时,狭义上指的是 ThreadPoolExectutor 及其子类,而广义上则指整个 Executor 大家族:

Executor:整个体系的最上级接口,定义了 execute 方法。ExecutorService:它在Executor接口的基础上,定义了 submit、shutdown 与 shutdownNow 等方法,完善了对 Future 接口的支持。AbstractExecutorService:实现了ExecutorService中关于任务提交的方法,将这部分逻辑统一为基于 execute 方法完成,使得实现类只需要关系 execute 方法的实现逻辑即可。ThreadPoolExecutor:线程池实现类,完善了线程状态管理与任务调度等具体的逻辑,实现了上述所有的接口。
ThreadPoolExecutor 作为 Executor 体系下最通用的实现基本可以满足日常的大部分需求,不过实际上也有不少定制的扩展实现,比如:
- JDK 基于
ThreadPoolExecutor实现了ScheduledThreadPoolExecutor用于支持任务调度。 - Tomcat 基于
ThreadPoolExecutor实现了一个同名的线程池,用于处理 Web 请求。 - Spring 基于
ExecutorService接口提供了一个ThreadPoolTaskExecutor实现,它仍然基于内置的ThreadPoolExecutor运行,在这个基础上提供了不少便捷的方法。
ThreadPoolExecutor 基于生产者与消费者模型实现,从功能上可以分为三个部分:
- 线程池本体:负责维护运行状态、管理工作线程以及调度任务。
- 工作队列:即在构造函数中指定的阻塞队列,它扮演者生产者消费者模型中缓冲区的角色。工作线程将会不断的从队列中获取并执行任务。
- 工作线程:即持有
Thread对象的内部类Worker,当一个Wroker被创建并启动以后,它将会不断的从工作队列中获取并执行任务,直到它因为获取任务超时、任务执行异常或线程池停机后才会终止运行。
当我们向线程池提交任务时,线程池将根据下述逻辑处理任务:
- 如果当前工作线程数小于核心线程数,则启动一个工作线程执行任务。
- 如果当前工作线程数大于等于核心线程数,且阻塞队列未满,则将任务添加到阻塞队列。
- 如果当前工作线程数大于等于核心线程数,且阻塞队列已满,则启动一个工作线程执行任务。
- 如果当前工作线程数已达最大值,且阻塞队列已满,则触发拒绝策略。

而当一个工作线程启动以后,它将会在一个 while 循环中重复执行下述逻辑:
- 通过
getTask方法从工作队列中获取任务,如果拿不到任务就阻塞一段时间,直到超时或者获取到任务。如果成功获取到任务就进入下一步,否则就直接进入线程退出流程; - 调用
Worker的lock方法加锁,保证一个线程只被一个任务占用; - 调用
beforeExecute回调方法,随后开始执行任务,如果在执行任务的过程中发生异常则会被捕获; - 任务执行完毕或者因为异常中断,此后调用一次
afterExecute回调方法,然后调用unlock方法解锁; - 如果线程是因为异常中断,那么进入线程退出流程,否则回到步骤 1 进入下一次循环。

线程池概要
1. 构造函数的参数
ThreadPoolExecutor 类一共提供了四个构造方法,我们基于参数最完整构造方法了解一下线程池创建所需要的变量:
public ThreadPoolExecutor(int corePoolSize, // 核心线程数
int maximumPoolSize, // 最大线程数
long keepAliveTime, // 非核心线程闲置存活时间
TimeUnit unit, // 时间单位
BlockingQueue<Runnable> workQueue, // 工作队列
ThreadFactory threadFactory, // 创建线程使用的线程工厂
RejectedExecutionHandler handler // 拒绝策略) {
}
- 核心线程数:即长期存在的线程数,当线程池中运行线程未达到核心线程数时会优先创建新线程**。**
- 最大线程数:当核心线程已满,工作队列已满,同时线程池中线程总数未超过最大线程数,会创建非核心线程。
- 超时时间:非核心线程闲置存活时间,当非核心线程闲置的时的最大存活时间。
- 时间单位:非核心线程闲置存活时间的时间单位。
- 任务队列:当核心线程满后,任务会优先加入工作队列,等待核心线程消费。
- 线程工厂:线程池创建新线程时使用的线程工厂。
- 拒绝策略:当工作队列已满,且线程池中线程数已经达到 最大线程数时,执行的兜底策略。
线程池每个参数的作用算是一个老生常谈的问题了,这里我们不过多赘述,你只需大概了解这几个参数即可,在下文我们会结合源码和具体的场景进一步的带你了解他们具体含义。
2. 工作线程 Worker
线程池的核心在于工作线程,在 ThreadPoolExecutor 中,每个工作线程都对应的一个内部类 Worker,它们都存放在一个 HashSet 中:
private final HashSet<Worker> workers = new HashSet<Worker>();
Worker 类的大致结构如下:
private final class Worker
extends AbstractQueuedSynchronizer
implements Runnable {
// 线程对象
final Thread thread;
// 首个执行的任务,一般执行完任务后就保持为空
Runnable firstTask;
// 该工作线程已经完成的任务数
volatile long completedTasks;
Worker(Runnable firstTask) {
// 默认状态为 -1,禁止中断直到线程启动为止
setState(-1);
this.firstTask = firstTask;
this.thread = getThreadFactory().newThread(this);
}
public void run() {
runWorker(this);
}
}
Worker 本身实现了 Runnable 接口,当创建一个 Worker 实例时,构造函数会通过我们在创建线程池时指定的线程工厂创建一个 Thread 对象,并把当前的 Worker 对象作为一个 Runnable 绑定到线程里面。当调用它的 run 方法时,它会通过调用线程池的 runWorker反过来启动线程,此时 Worker 就开始运行了。
Worker 类继承了 AbstractQueuedSynchronizer,也就是我们一般说的 AQS,这意味着当我们操作 Worker 的时候,它会通过 AQS 的同步机制来保证对工作线程的访问是线程安全。比如当工作线程开始执行任务时,就会“加锁”,直到任务执行结束以后才会“解锁”。
3. 主锁 mainLock
在上文介绍工作线程的时候,我们会注意到,线程池直接使用一个 HashSet 来存储 Worker 示例,而 HashSet 本身却并非线程安全的,那在并发场景下要如何保证线程安全呢?
实际上,除了 workers 以外,线程池中还有大量非线程安全的变量,这里再举几个例子:
ctl:记录线程池状态与工作线程数。largestPoolSize/corePoolSize:最大/核心工作线程数。completedTaskCount:已完成任务数。keepAliveTime:核心线程超时时间。
这些变量实际上环环相扣,因此很难通过分别将它们改为原子变量/并发容器来保证线程安全,因此 ThreadPoolExecutor 选择为整个线程池提供一把主锁 mainLock,每次操作或读取这种全局性变量的时候,都需要获取主锁才能进行:
private final ReentrantLock mainLock = new ReentrantLock();
比如获取当前工作线程数的时候:
public int getPoolSize() {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
// 如果线程已经开始停机,则返回 0,否则返回工作线程数量
return runStateAtLeast(ctl.get(), TIDYING) ? 0
: workers.size();
} finally {
mainLock.unlock();
}
}
总的来说,线程池通过 mainLock 来保证全局配置的线程安全,而每个工作线程再通过 AQS 来保证工作线程自己的线程安全。
状态控制
1. ctl
线程池拥有一个 AtomicInteger 类型的成员变量 ctl ,它是 control 的缩写,线程池分别通过 ctl 的高位低位来管理两部分状态信息:
- 第一部分为高 3 位,用来记录线程池当前的运行状态。
- 第二部分为低 29 位,用来记录线程池中的工作线程数。

private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
// 29(32-3)
private static final int COUNT_BITS = Integer.SIZE - 3;
// ======== 线程数相关常量 ========
// 允许的最大工作线程(2^29-1 约5亿)
private static final int CAPACITY = (1 << COUNT_BITS) - 1;
// ======== 线程状态相关常量 ========
// 运行状态。线程池接受并处理新任务
private static final int RUNNING = -1 << COUNT_BITS;
// 关闭状态。线程池不能接受新任务,处理完剩余任务后关闭。调用shutdown()方法会进入该状态。
private static final int SHUTDOWN = 0 << COUNT_BITS;
// 停止状态。线程池不能接受新任务,并且尝试中断旧任务。调用shutdownNow()方法会进入该状态。
private static final int STOP = 1 << COUNT_BITS;
// 整理状态。由关闭状态转变,线程池任务队列为空时进入该状态,会调用terminated()方法。
private static final int TIDYING = 2 << COUNT_BITS;
// 终止状态。terminated()方法执行完毕后进入该状态,线程池彻底停止。
private static final int TERMINATED = 3 << COUNT_BITS;
在实际使用时,线程池将会通过位运算从 ctl 变量中解析出所需要的部分,并且做出相应的修改。
2. 通过 clt 计算当前状态
在开始前,我们需要理解一下关于补码的知识:在计算机中,二进制数中的首位是符号位,即第一位为 0 时表示正数,为 1 时表示负数。当我们需要表示一个负数时,则需要对它对应的正数按位取反再 + 1(也 就是先取反码再获得补码)。
举个例子,如果我们假设在代码中使用 1 字节 —— 也就是 8 bit,即 8 位 —— 来表示一个数字,那么这种情况下,1 的二进制表示方式为 0000 0001,而 -1 则为 1111 1111

现在我们对补码有了基础的了解,那么就可以尝试着理解线程池是如何通过 -1 << COUNT_BITS这行代码来表示 RUNNING 这个状态的:
- Java 中 int 是 4 个字节,即 32 bit,按上述过程 -1 这个值转为二进制即为 1 111......1111(32个1);
COUNT_BITS是 29,-1 << COUNT_BITS这行代码表示让 -1 左移 29 位。- 我们对 -1 左移 29 位后得到了一个 32 位的 int 值,它转为二进制就是 1110...0000,即前 3 位为 1,其余 29 位都为 0。
同理,计算其他的几种状态,最终可知五种状态对应的二进制表示分别是:
| 状态 | 二进制 |
|---|---|
| RUNNING | 1110...0....00(可记为 111) |
| SHUTDOWN | 0000...0....00(可记为 000) |
| STOP | 0010...0....00(可记为 001) |
| TIDYING | 0100...0....00(可记为 010) |
| TERMINATED | 0110...0....00(可记为 011) |
有意思的地方在于,RUNNING 的符号位是 1,说明它转为十进制以后是个负数,而除它以外其他的状态的符号位都是 0,转为十进制之后都是正数,也就是说,我们可以这么认为:
小于 SHUTDOWN 的就是 RUNNING,大于 SHUTDOWN 就是停止中或者已停止。
这也是后面状态计算的一些写法的基础。比如 isRunning()方法:
private static boolean isRunning(int c) {
return c < SHUTDOWN;
}
3. 通过 ctl 计算工作线程数
在代 码中,我们会经常看到线程池通过这一段代码来获取状态:
// 29(32-3)
private static final int COUNT_BITS = Integer.SIZE - 3;
// 允许的最大工作线程(2^29-1 约5亿)
private static final int CAPACITY = (1 << COUNT_BITS) - 1;
private static int workerCountOf(int c) {
return c & CAPACITY;
}
这个运算过程其实与上文运行状态的运算过程基本一致:
- 在 Java 中,int 值 1 的二进制表示为 0000……1,即除了最后一位为 1 以外其他都为 0;
- 然后
1 << COUNT_BITS表示让 1 左移 29 位,即得到 0010……0,即除了第三位为 1 以外其他位都为 0; - 随后对该值再 -1,即得到 0001……1,即除了前三位为 0 以外,其余 29 位皆为 1,此时该值实际上就是一个掩码了;
- 此后,我们执行
c & CAPACITY,实际上就会得到低 29 位的值,即当前线程中的线程数量。
这里我们举个例子,假如当前线程池处于 RUNNING 状态,且有 1 个工作线程,那么此时 ctl 值为 1110……0001,即前三位和最后一位为 1,其余位数都为 0,然后与 CAPACITY 进行与运算后,高三位全变为 0,此时 ctl 即为 0000……0001,也就是 1。
线程池状态
1. 状态的流转
在前文,我们提到线程池通过 ctl 一共可以表示五种状态:
- RUNNING:运行状态。线程池接受并处理新任务。
- SHUTDOWN :关闭状态。线程池不能接受新任务,处理完剩余任务后关闭。调用
shutdown方法会进入该状态。 - STOP:停止状态。线程池不能接受新任务,并且尝试中断旧任务。调用
shutdownNow方法会进入该状态。 - TIDYING:整理状态。由关闭状态转变,线程池任务队列为空且没有任何工作线程时时进入该状态,会调用
terminated方法。 - TERMINATED:终止状态。
terminated方法执行完毕后进入该状态,线程池彻底停止。
它们具体的流转关系可以参考下图:

除了这种运行状态,线程池还提供了一些方法让我们可以获取其他的指标信息,比如核心线程数、最大线程数等,这个咱们后面会详细讲。
2. 如何触发停机?
当我们要停止运行一个线程池时,可以调用下述两个方法:
- shutdown:中断线程池,不再添加新任务,同时等待当前进行和队列中的任务完成