孤舟笔记 并发篇二十三 线程池是如何实现线程复用的?Worker循环取任务的秘密远比你想象的精巧

文章目录

个人网站

每个 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 循环:

  1. 首次:执行 Worker 绑定的 firstTask
  2. 后续:task = getTask() 从队列取
  3. 取到了 → 执行 → task 置 null → 下一轮继续取
  4. 取不到 → 循环退出 → 线程终止

线程没有退出,是因为 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() 能取到任务,线程就不会退出,从而实现复用。

加分回答
  1. Worker 的 AQS 锁设计:Worker 继承 AQS 实现了不可重入的互斥锁,用于标记线程是否正在执行任务。shutdown() 时只中断空闲线程(锁未被占用),不中断正在执行任务的线程。不可重入是为了防止任务代码中调用 shutdown() 时误判线程状态
  2. beforeExecute/afterExecute 钩子:runWorker() 在任务执行前后留了钩子方法,子类可以覆盖来实现监控、统计、日志等功能。这是模板方法模式的体现
  3. 线程复用 vs 对象池:线程池的复用不是"对象池"那种借还模式,而是"循环取任务"模式。线程始终持有运行权,只是在不同任务之间切换。这种设计更高效,避免了线程的频繁创建和上下文切换
面试官点评

这道题考的是你对线程池内部运作机制的深入理解。能说出"while 循环取任务"是核心,但能讲清 Worker 的设计(AQS 锁、firstTask、getTask 的区别)、核心线程和非核心线程的不同等待策略、以及 beforeExecute/afterExecute 钩子的作用,才说明你真的读过源码。面试官最想听到的核心认知是:线程复用的本质不是"线程被回收再分配",而是"线程一直在循环中跑,只是执行的任务在变"。

原文阅读


内容有帮助?点赞、收藏、关注三连!评论区等你 💪

相关推荐
落魄江湖行2 天前
孤舟笔记 并发篇十一 行锁、间隙锁、临键锁傻傻分不清?MySQL InnoDB的锁其实就这三板斧
mysql·java并发·春招·孤舟笔记
落魄江湖行2 天前
孤舟笔记 并发篇十 ReentrantLock的公平锁和非公平锁是怎么实现的?这个设计藏着大智慧
java并发·春招·孤舟笔记
逻辑驱动的ken3 天前
Java高频面试考点场景题17
开发语言·jvm·面试·求职招聘·春招
逻辑驱动的ken5 天前
Java高频面试考点场景题14
java·开发语言·深度学习·面试·职场和发展·求职招聘·春招
实习僧企业版8 天前
如何为中小企业点亮校招吸引力的灯塔
大数据·春招·雇主品牌·招聘技巧·口碑
逻辑驱动的ken8 天前
Java高频面试考点场景题13
java·开发语言·jvm·面试·求职招聘·春招
Javatutouhouduan9 天前
阿里2026最新Java面试核心讲(终极版)
java·java面试·java并发·后端开发·java程序员·java八股文·java性能优化
逻辑驱动的ken11 天前
Java高频面试考点场景题10
java·开发语言·深度学习·求职招聘·春招
逻辑驱动的ken13 天前
Java高频面试考点场景题08
java·开发语言·面试·求职招聘·春招