文章目录
-
- 一、先说结论:线程复用的核心机制
- 二、Worker:线程和任务的"合体"
- 三、runWorker():复用的核心循环
- 四、getTask():从队列取下一个任务
- 五、完整流程:从提交到复用
- [六、对比:不使用线程池 vs 使用线程池](#六、对比:不使用线程池 vs 使用线程池)
- 线程复用机制全景
- 回答技巧与点评
每个 Thread 只能 start 一次,那线程池怎么做到"复用"线程的?一个线程执行完任务为什么没有退出?它是怎么拿到下一个任务的?
搞不懂线程复用的原理,线程池对你来说就是黑盒。今天咱们拆开看看。
一、先说结论:线程复用的核心机制
| 维度 | 说明 |
|---|---|
| 核心思路 | Worker 线程在 while 循环中反复取任务,取到就执行,取不到就等 |
| 关键代码 | runWorker() 中的 while (task != null || (task = getTask()) != null) |
| Worker 本质 | 继承 AQS,既是线程又是任务载体 |
| 首次任务 | Worker 创建时绑定 firstTask,先执行它 |
| 后续任务 | firstTask 执行完后,从 workQueue 中取 |
| 等待机制 | 核心线程用 take() 永久阻塞,非核心线程用 poll() 超时等待 |
一句话记住:线程复用就像流水线工人------干完一个零件不是下班,而是伸手拿下一个,没零件就等着,永远不会自己走。
二、Worker:线程和任务的"合体"
Worker 是 ThreadPoolExecutor 的内部类,它同时继承了 AQS 并实现了 Runnable:
java
private final class Worker extends AQS implements Runnable {
final Thread thread; // Worker 对应的线程 👈
Runnable firstTask; // 第一个任务(可能为 null) 👈
volatile long completedTasks;
Worker(Runnable firstTask) {
setState(-1); // 禁止在 runWorker 前被中断
this.firstTask = firstTask;
this.thread = getThreadFactory().newThread(this); // 用自己创建线程 👈
}
public void run() {
runWorker(this); // 线程启动后执行 runWorker 👈
}
}
关键: Worker 本身就是 Runnable,thread 的 run() 方法执行的就是 Worker.run() → runWorker()。线程启动后,就进入了 runWorker 的循环。
三、runWorker():复用的核心循环
java
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask; // 先拿第一个任务 👈
w.firstTask = null;
w.unlock(); // 允许被中断
try {
while (task != null || (task = getTask()) != null) { // 👈 核心循环
w.lock(); // 执行任务时加锁(防止 shutdown 同时中断)
// 检查线程池状态
if ((runStateAtLeast(ctl.get(), STOP) ||
(Thread.interrupted() &&
runStateAtLeast(ctl.get(), STOP))) &&
!wt.isInterrupted())
wt.interrupt();
try {
beforeExecute(wt, task); // 钩子方法
try {
task.run(); // 👈 执行任务!
} catch (Throwable x) {
afterExecute(task, x); // 钩子方法
throw x;
} finally {
afterExecute(task, null); // 钩子方法
}
} finally {
task = null; // 清空,下一轮从队列取 👈
w.completedTasks++;
w.unlock();
}
}
} finally {
processWorkerExit(w, false); // 线程退出 👈
}
}
复用的秘密全在 while 循环:
- 首次:执行 Worker 绑定的 firstTask
- 后续:
task = getTask()从队列取 - 取到了 → 执行 → task 置 null → 下一轮继续取
- 取不到 → 循环退出 → 线程终止
线程没有退出,是因为 while 循环没结束。 这就是复用的本质。
四、getTask():从队列取下一个任务
java
private Runnable getTask() {
boolean timedOut = false;
for (;;) {
// ... 状态检查 ...
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
try {
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) : // 限时等 👈
workQueue.take(); // 永久等 👈
if (r != null)
return r;
timedOut = true;
} catch (InterruptedException retry) {
timedOut = false;
}
}
}
| 场景 | 取任务方式 | 行为 |
|---|---|---|
| 核心线程 | take() | 队列空时永久阻塞,直到有任务 |
| 非核心线程 | poll(keepAliveTime) | 队列空时等待 keepAliveTime,超时返回 null |
| 队列有任务 | poll/take 立即返回 | 拿到任务继续执行 |
生活类比: 核心线程是正式员工,在工位上一直等(take);非核心线程是临时工,等了 keepAliveTime 还没事干就下班了(poll 超时)。
五、完整流程:从提交到复用
提交任务 submit(task)
│
├─ workerCount < corePoolSize?
│ └─ 是 → addWorker(task, true) → 创建 Worker,绑定 firstTask 👈
│
├─ workQueue.offer(task)?
│ └─ 是 → 任务入队,等待空闲 Worker 来取
│
└─ 否 → addWorker(task, false) → 创建非核心 Worker
Worker 生命周期
│
start() → run() → runWorker()
│
while 循环:
├─ 执行 firstTask(第一次)
├─ getTask() 从队列取(后续) 👈 复用的关键
├─ 取到 → task.run()
├─ 取不到 → 循环退出 → processWorkerExit()
└─ 线程终止
六、对比:不使用线程池 vs 使用线程池
java
// 不用线程池:每个任务一个线程,执行完就销毁
for (int i = 0; i < 1000; i++) {
new Thread(task).start(); // 创建1000个线程,用完即销毁 👈
}
// 线程池:10个线程循环执行1000个任务
ExecutorService pool = Executors.newFixedThreadPool(10);
for (int i = 0; i < 1000; i++) {
pool.submit(task); // 10个Worker循环取任务,复用线程 👈
}
节省了 990 个线程的创建和销毁开销。
线程复用机制全景
线程复用 全景
核心原理
Worker 线程在 while 循环中反复取任务
├── firstTask → 首次执行
├── getTask() → 后续从队列取
└── 循环退出 → 线程终止(不再复用)
关键方法
├── runWorker() ── while 循环取任务并执行
├── getTask() ── 从 workQueue 获取任务
│ ├── take() ── 核心线程永久等
│ └── poll(timeout) ── 非核心线程限时等
└── processWorkerExit() ── 线程退出善后
Worker 设计
├── extends AQS ── 实现不可重入锁(标记线程是否在执行任务)
├── implements Runnable ── 本身是可执行体
└── thread = new Thread(this) ── 用自己创建线程
口诀:Worker 是循环工,firstTask 先干完,
getTask 从队列取,取到就干取不到等,
核心永远等,非核心限时等,
while 循环不停歇,线程复用自然成。
回答技巧与点评
标准回答
线程池通过 Worker 线程的 while 循环实现线程复用。Worker 继承 AQS 并实现 Runnable,创建时绑定一个 firstTask。线程启动后执行 runWorker(),在 while 循环中先执行 firstTask,然后不断通过 getTask() 从 workQueue 获取下一个任务。核心线程使用 take() 永久阻塞等待,非核心线程使用 poll(keepAliveTime) 限时等待。只要 getTask() 能取到任务,线程就不会退出,从而实现复用。
加分回答
- Worker 的 AQS 锁设计:Worker 继承 AQS 实现了不可重入的互斥锁,用于标记线程是否正在执行任务。shutdown() 时只中断空闲线程(锁未被占用),不中断正在执行任务的线程。不可重入是为了防止任务代码中调用 shutdown() 时误判线程状态
- beforeExecute/afterExecute 钩子:runWorker() 在任务执行前后留了钩子方法,子类可以覆盖来实现监控、统计、日志等功能。这是模板方法模式的体现
- 线程复用 vs 对象池:线程池的复用不是"对象池"那种借还模式,而是"循环取任务"模式。线程始终持有运行权,只是在不同任务之间切换。这种设计更高效,避免了线程的频繁创建和上下文切换
面试官点评
这道题考的是你对线程池内部运作机制的深入理解。能说出"while 循环取任务"是核心,但能讲清 Worker 的设计(AQS 锁、firstTask、getTask 的区别)、核心线程和非核心线程的不同等待策略、以及 beforeExecute/afterExecute 钩子的作用,才说明你真的读过源码。面试官最想听到的核心认知是:线程复用的本质不是"线程被回收再分配",而是"线程一直在循环中跑,只是执行的任务在变"。
内容有帮助?点赞、收藏、关注三连!评论区等你 💪