孤舟笔记 并发篇二十三 线程池是如何实现线程复用的?Worker循环取任务的秘密远比你想象的精巧
文章目录
- 一、先说结论:线程复用的核心机制
- 二、Worker:线程和任务的"合体"
- 三、runWorker():复用的核心循环
- 四、getTask():从队列取下一个任务
- 五、完整流程:从提交到复用
- 六、对比:不使用线程池 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:
privatefinalclassWorkerextendsAQSimplementsRunnable{finalThreadthread;// Worker 对应的线程 👈RunnablefirstTask;// 第一个任务(可能为 null) 👈volatilelongcompletedTasks;Worker(RunnablefirstTask){setState(-1);// 禁止在 runWorker 前被中断this.firstTask=firstTask;this.thread=getThreadFactory().newThread(this);// 用自己创建线程 👈}publicvoidrun(){runWorker(this);// 线程启动后执行 runWorker 👈}}关键:Worker 本身就是 Runnable,thread 的 run() 方法执行的就是 Worker.run() → runWorker()。线程启动后,就进入了 runWorker 的循环。
三、runWorker():复用的核心循环
finalvoidrunWorker(Workerw){Threadwt=Thread.currentThread();Runnabletask=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(Throwablex){afterExecute(task,x);// 钩子方法throwx;}finally{afterExecute(task,null);// 钩子方法}}finally{task=null;// 清空,下一轮从队列取 👈w.completedTasks++;w.unlock();}}}finally{processWorkerExit(w,false);// 线程退出 👈}}复用的秘密全在 while 循环:
- 首次:执行 Worker 绑定的 firstTask
- 后续:
task = getTask()从队列取 - 取到了 → 执行 → task 置 null → 下一轮继续取
- 取不到 → 循环退出 → 线程终止
线程没有退出,是因为 while 循环没结束。这就是复用的本质。
四、getTask():从队列取下一个任务
privateRunnablegetTask(){booleantimedOut=false;for(;;){// ... 状态检查 ...booleantimed=allowCoreThreadTimeOut||wc>corePoolSize;try{Runnabler=timed?workQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS):// 限时等 👈workQueue.take();// 永久等 👈if(r!=null)returnr;timedOut=true;}catch(InterruptedExceptionretry){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 使用线程池
// 不用线程池:每个任务一个线程,执行完就销毁for(inti=0;i<1000;i++){newThread(task).start();// 创建1000个线程,用完即销毁 👈}// 线程池:10个线程循环执行1000个任务ExecutorServicepool=Executors.newFixedThreadPool(10);for(inti=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 钩子的作用,才说明你真的读过源码。面试官最想听到的核心认知是:线程复用的本质不是"线程被回收再分配",而是"线程一直在循环中跑,只是执行的任务在变"。
原文阅读
内容有帮助?点赞、收藏、关注三连!评论区等你 💪
