孤舟笔记 并发篇二十二 线程池是如何回收线程的?核心线程和非核心线程的回收逻辑大不相同

文章目录

个人网站

线程池里那么多线程,什么时候回收?是所有线程都回收,还是只回收一部分?核心线程真的永远不会被回收吗?这些问题不搞清楚,线上线程数暴涨你还不知道为什么。

今天咱们深入源码,把线程池的线程回收机制彻底讲透。

一、先说结论:线程回收核心规则

维度 说明
非核心线程 空闲超过 keepAliveTime 自动回收
核心线程 默认不回收,即使空闲
allowCoreThreadTimeOut 设为 true 后,核心线程也参与超时回收
回收本质 Worker 的 runWorker() 循环取任务失败,线程自然退出
回收时机 getTask() 返回 null → 线程退出 run() → 线程终止

一句话记住:非核心线程像临时工,没活就走;核心线程像正式工,默认终身雇佣------除非公司开了 allowCoreThreadTimeOut 这个"裁员开关"。

二、回收的本质:getTask() 返回 null

线程池中每个线程都被包装成 Worker,Worker 的核心逻辑是 runWorker():

java 复制代码
// ThreadPoolExecutor.Worker(简化)
final void runWorker(Worker w) {
    try {
        while (task != null || (task = getTask()) != null) {  // 👈 关键循环
            try {
                task.run();  // 执行任务
            } finally {
                task = null;
            }
        }
    } finally {
        processWorkerExit(w, false);  // 线程退出清理 👈
    }
}

核心逻辑: Worker 线程在一个 while 循环中不断通过 getTask() 获取任务。如果 getTask() 返回 null,循环退出,线程自然终止。

所以"回收线程"的本质就是让 getTask() 返回 null。

三、getTask():回收的决策中心

java 复制代码
private Runnable getTask() {
    boolean timedOut = false;
    for (;;) {
        int c = ctl.get();
        int wc = workerCountOf(c);

        // 是否允许超时回收?
        boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;  // 👈

        // 满足回收条件:线程数超过核心数 且 (超时 或 线程数过多)
        if ((wc > maximumPoolSize || (timed && timedOut))
            && (wc > 1 || workQueue.isEmpty())) {
            if (compareAndDecrementWorkerCount(c))
                return null;  // 👈 返回 null,线程退出!
            continue;
        }

        try {
            Runnable r = timed ?
                workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :  // 超时等待 👈
                workQueue.take();  // 永久等待(核心线程) 👈
            if (r != null)
                return r;
            timedOut = true;  // poll 超时了
        } catch (InterruptedException retry) {
            timedOut = false;
        }
    }
}

关键变量 timed

  • timed = true:使用 poll(keepAliveTime),超时返回 null → 线程退出
  • timed = false:使用 take(),永久阻塞 → 线程一直等

四、核心线程 vs 非核心线程的回收逻辑

默认情况(allowCoreThreadTimeOut = false)

复制代码
timed = allowCoreThreadTimeOut || workerCount > corePoolSize

- 核心线程(workerCount ≤ corePoolSize):timed = false → take() 永久等待 → 不回收 👈
- 非核心线程(workerCount > corePoolSize):timed = true → poll(keepAliveTime) → 超时回收 👈

生活类比: 正式工下班后在休息室等(永远等),临时工下班后看表计时,时间到了走人。

开启 allowCoreThreadTimeOut

java 复制代码
pool.allowCoreThreadTimeOut(true);  // 👈 开启核心线程超时回收

此时 timed 始终为 true,所有线程都用 poll(keepAliveTime),空闲超时都回收。

五、processWorkerExit:退出后的善后

线程退出后,processWorkerExit() 负责清理:

java 复制代码
private void processWorkerExit(Worker w, boolean completedAbruptly) {
    // 1. 从 workers 集合中移除
    workers.remove(w);
    
    // 2. 工作线程数 -1
    decrementWorkerCount();
    
    // 3. 尝试终止线程池(如果是最后一个线程)
    tryTerminate();
    
    // 4. 如果线程数低于核心数,补充一个新 Worker
    int c = ctl.get();
    if (runStateLessThan(c, STOP)) {
        if (!completedAbruptly) {
            int min = allowCoreThreadTimeOut ? 0 : corePoolSize;
            if (min == 0 && !workQueue.isEmpty())
                min = 1;
            if (workerCountOf(c) >= min)
                return;
        }
        addWorker(null, false);  // 补充 Worker 👈
    }
}

注意第 4 步: 如果线程退出后,工作线程数低于 corePoolSize,会自动补充新 Worker!这保证了核心线程数的稳定性。

六、常见误区

误区 1:核心线程永远不会退出

→ 开启 allowCoreThreadTimeOut 后核心线程也会超时退出

误区 2:线程池关闭后线程立刻终止

→ shutdown() 会等所有任务执行完,shutdownNow() 会中断所有线程

误区 3:空闲线程消耗 CPU

→ 空闲核心线程阻塞在 take(),不消耗 CPU,只占内存(约 1MB 栈空间)

线程回收机制全景

复制代码
线程池线程回收 全景

回收条件
├── 非核心线程:空闲超过 keepAliveTime → poll() 返回 null → 退出
├── 核心线程(默认):take() 永久等待 → 不回收
└── 核心线程(allowCoreThreadTimeOut=true):同样 poll() 超时回收

回收流程
getTask() 返回 null
  → runWorker() 循环退出
  → processWorkerExit() 清理
  → 线程自然终止

自动补偿
线程退出后,若 workerCount < corePoolSize → 自动 addWorker()

内存影响
空闲核心线程阻塞在 take() → 不占 CPU,占 ~1MB 栈内存

口诀:非核心看闹钟,超时就走人,
      核心默认等永久,开开关也回收,
      getTask 返回 null,线程自然终止,
      少了会补人,核心数保得住。

回答技巧与点评

标准回答

线程池通过 Worker 线程循环调用 getTask() 获取任务来工作。非核心线程使用 poll(keepAliveTime) 从队列获取任务,超时返回 null 导致线程退出;核心线程默认使用 take() 永久等待,不会被回收。通过 allowCoreThreadTimeOut(true) 可以让核心线程也参与超时回收。线程退出后会执行 processWorkerExit() 清理,如果线程数低于 corePoolSize 还会自动补充 Worker。

加分回答
  1. 核心线程的内存开销:空闲核心线程虽然不消耗 CPU,但每个线程约占用 1MB 栈空间。如果 corePoolSize 设得很大且长时间空闲,是内存浪费。开启 allowCoreThreadTimeOut 可以在空闲时回收核心线程,节省内存
  2. 线程池关闭时的回收:shutdown() 调用后不再接受新任务,但会等已有任务执行完,然后所有线程自然退出(getTask 返回 null)。shutdownNow() 会设置中断标志并尝试中断所有线程,正在执行的任务可能被中断
  3. 动态调整的影响:运行时调大 corePoolSize 不会立即创建线程,只在下次提交任务时补充;调小 corePoolSize 不会直接回收线程,而是等非核心线程自然超时退出或下次 getTask 时判断回收
面试官点评

这道题考的是你对线程池内部机制的深入理解。能说出"非核心线程超时回收"只是表面,能讲清 getTask() 的 timed 判断逻辑、allowCoreThreadTimeOut 的作用、以及 processWorkerExit 的自动补充机制,才说明你真的看过源码。面试官最想听到的是:你理解回收的"本质"是 getTask() 返回 null 导致循环退出,而不是什么"魔法回收"。

原文阅读


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

相关推荐
落魄江湖行12 小时前
孤舟笔记 并发篇二十五 当任务数超过核心线程数时,如何让任务不进入队列?线程池调优的经典问题
java并发·春招·孤舟笔记·当任务数超过核心线程数时
落魄江湖行13 小时前
孤舟笔记 并发篇二十三 线程池是如何实现线程复用的?Worker循环取任务的秘密远比你想象的精巧
java并发·春招·孤舟笔记
落魄江湖行2 天前
孤舟笔记 并发篇十一 行锁、间隙锁、临键锁傻傻分不清?MySQL InnoDB的锁其实就这三板斧
mysql·java并发·春招·孤舟笔记
落魄江湖行3 天前
孤舟笔记 并发篇十 ReentrantLock的公平锁和非公平锁是怎么实现的?这个设计藏着大智慧
java并发·春招·孤舟笔记
逻辑驱动的ken3 天前
Java高频面试考点场景题17
开发语言·jvm·面试·求职招聘·春招
逻辑驱动的ken6 天前
Java高频面试考点场景题14
java·开发语言·深度学习·面试·职场和发展·求职招聘·春招
实习僧企业版8 天前
如何为中小企业点亮校招吸引力的灯塔
大数据·春招·雇主品牌·招聘技巧·口碑
逻辑驱动的ken8 天前
Java高频面试考点场景题13
java·开发语言·jvm·面试·求职招聘·春招
Javatutouhouduan9 天前
阿里2026最新Java面试核心讲(终极版)
java·java面试·java并发·后端开发·java程序员·java八股文·java性能优化