JDK 21 · THREADPOOLEXECUTOR · CTL · ADDWORKER · RUNWORKER · GETTASK · WORKER · MAINLOCK

三个 if、一个 int、一把不可重入的锁

execute 只有三个分支:核心名额没满,就开线程,把任务直接交给它;否则放进队列,放完再看一眼池子还开着没有、还有没有线程;放不进就开临时线程,开不了就拒绝。池子的运行状态和线程数挤在一个 int(ctl)里,所以“看状态、占名额”一次 CAS 就能做完。线程要退出时,也会回头看一眼队列。两边各查一次,任务就不会卡在没人取的队列里。

这是下一圈线程池“先补核心、再排队、再加临时线程、最后拒绝”的四步规则,在 Spring @Async 与线程池 讲过,还有模拟器。这一页接着往下:execute 的源码为什么是这三个 if,线程怎么被造出来、怎么取任务、怎么退出。所以 ①~⑤ 都很短,篇幅给 ⑥ 的源码和实验台。假设场景是面试被追问“讲讲 execute 的源码”,以及线上遇到几件只有看源码才说得清的怪事。源码以 JDK 21u 为准,标“实测”的在本机跑过:JDK 11.0.32 和 21.0.12。

①为什么需要:四步规则背不出来的几件事

只记四步规则,下面这几件事都解释不了。它们都是本页 ⑥ 实测过的:

?核心数设成 0、最大数 10、无界队列

10 个任务进来,自始至终只有 1 个线程在干活。第 1 步跳过、第 3 步走不到,那这 1 个线程是谁建的?

?任务里抛了个异常

线程名从 pool-1-thread-1 变成了 pool-1-thread-2。换成 submit 提交,异常却连日志都没有。

?配了 CallerRunsPolicy,说好“满了让调用方自己跑”

关池以后提交的 100 个任务,一个都没跑,也没有异常。

?“核心线程”不会被回收

可实测里先建的那个线程被回收了,留下来的是后建的“临时线程”。

②没有它会怎样:每个任务一个新线程

不用线程池,每来一个任务就 new Thread(task).start():建线程、销毁线程都要进内核,突发流量时线程数没有上限。线程池的四步规则就是在“复用线程”和“限住总数”之间找平衡,旧页有完整的图和模拟器,这里不重讲。

③相邻方案:JDK 里还有哪些执行器

执行器线程从哪来任务放哪适合
ThreadPoolExecutor按核心数、最大数造,闲久了回收你给的 BlockingQueue一般的业务异步、IO 任务
ForkJoinPool固定并行度,每个线程一个双端队列,空闲的去偷别人的各线程自己的队列能拆分的计算任务;parallelStream、CompletableFuture 默认用它
Executors.newVirtualThreadPerTaskExecutor()每个任务一个虚拟线程,不复用不排队JDK 21 起,大量阻塞 IO 的任务
Spring ThreadPoolTaskExecutor里面就是一个 ThreadPoolExecutor同左Spring 里配置更方便,规则完全一样

本页学 ThreadPoolExecutor:Spring 的线程池、Executors.newFixedThreadPool、newCachedThreadPool 背后都是它,execute 是所有任务的入口。

④原理:四步规则落到源码里是什么样

四步规则在 旧页讲过:先补满核心线程,再排队,队列满了加临时线程,再不行就拒绝。这一圈先认识源码里撑起这四步的几样东西,再看三个 if 怎么排。

这一圈要用到的四样东西

ctl

一个 AtomicInteger,高 3 位是池子的运行状态,低 29 位是线程数。看状态、改线程数都是对它做 CAS。

Worker

一个工作线程的“外壳”:里面有真正的 Thread、一个 firstTask(建它时顺手带来的第一个任务),自己还是一把锁。

workers + mainLock

所有 Worker 放在一个普通的 HashSet 里,进出都要先拿 mainLock(一把 ReentrantLock)。

workQueue

你给的阻塞队列。工作线程空闲时停在它的 take() 或 poll(keepAlive) 上等任务。

机制 1execute 的三个 if

execute(task) ① 读 ctl:线程数 < corePoolSize?第 1363 行 是 addWorker(task, true):开线程,task 直接交给它成功就返回;失败(被别人抢了名额、池子关了)往下走 否 / 失败 ② 还在 RUNNING 且 offer 成功?第 1368 行 是 放进去之后,再读一次 ctl(复查) 池子已经不在 RUNNING → 把 task 撤回,拒绝 线程数是 0 → addWorker(null, false) 补一个线程 都不是 → 返回,等工作线程从队列里取 否(队列满了 / 关了) ③ addWorker(task, false)开临时线程,task 直接交给它 失败 reject(task):交给拒绝策略 “临时线程插队”就是这里来的: 新线程的第一个任务是刚来的 task, 不经过队列,比队列里排着的先跑。
四步规则只有三个 if,因为“队列满了才加临时线程”和“临时线程也加不了就拒绝”合在第 ③ 个分支里。容易忽略的是第 ② 步里的复查:它在第 1 次读 ctl 和真正放进队列之间,池子可能被关了,最后一个线程也可能刚好退出了。核心数为 0 的池子全靠它建出第一个线程,见 ⑥ 坑 1。

机制 2一个 int 装两样东西

111高 3 位:runState 00000000000000000000000000100低 29 位:workerCount(最多 2^29 − 1,约 5.4 亿) 上面这个值是 ⑤ 实测打印的:RUNNING,4 个线程。状态按数值从小到大: RUNNING111(负数) SHUTDOWN000 STOP001 TIDYING010 TERMINATED011
RUNNING 是 -1 << 29,是负数,其余状态依次变大,所以“还在跑吗”就是 c < SHUTDOWN,“至少停到哪一步”也是一次整数比较。shutdown() 把状态改成 SHUTDOWN(不再收新任务,队列里的照跑),shutdownNow() 改成 STOP(还要打断正在跑的、退回队列里的);线程数归零、队列也空了,经过 TIDYING 到 TERMINATED。

机制 3一个工作线程的一生

addWorkerCAS ctl 先占名额再拿 mainLock 建 Worker、启动 runWorker先跑 firstTask每个任务前后锁住自己 getTask线程数 ≤ 核心数:take() 一直等否则 poll(keepAlive) 限时等 取到任务 → 跑完 → 再取 null processWorkerExit离开 workers getTask 什么时候返回 null(线程退出): · 池子 SHUTDOWN 了而且队列空了,或者已经 STOP; · 等满 keepAlive 没等到任务,而且当时线程数 > 核心数(或允许核心线程超时),并且自己不是最后一个线程、或者队列是空的。 退出前(processWorkerExit)再看一眼:队列不空却没线程了,就再 addWorker 一个。
注意“线程数 > 核心数”是每次进 getTask 时现算的:池子里没有哪个线程被标成“核心线程”,谁先闲满 keepAlive 谁就走,剩下的就算核心。任务抛异常时,异常一路抛出 runWorker,线程照样走 processWorkerExit,然后被换掉。

机制 4两边各查一次:任务不会卡在没人取的队列里

提交的线程 最后一个要退出的线程 ① workQueue.offer(task) ② 复查 ctl:线程数 0 → 补一个 ① CAS 线程数 −1 ② 看队列:不空却没线程 → 补一个 为什么够了: 两边都是“先改自己那一边,再读对方那一边”。不管怎么交错,至少有一边能读到对方改过的值: 提交者看到线程数已经是 0,或者退出的线程看到队列里有任务。任何一边看到了,就会补一个线程。
这和 ConcurrentBag、SynchronousQueue 里“先写自己的、再读对方的”是同一个套路,第三次见到了。getTask 里还有一道:最后一个线程看到队列不空时根本不退出(wc > 1 || workQueue.isEmpty())。⑥ 的实验台和随机测试会拆掉其中一边看看会怎样。

为什么这样设计:状态和线程数必须一起 CAS

被放弃的:两个变量runState 一个、workerCount 一个

  • addWorker 要做的是“还在 RUNNING、名额没满,才 +1”;tryTerminate 要做的是“已经关了、线程数是 0,才进 TIDYING”。两件事都是“看两个值,再改其中一个”
  • 两个变量各自 CAS,挡不住这样的交错:一边刚判定可以终止,另一边同时把线程数 +1,后面只能靠别处再补救
  • 想不交错,就得每次都拿一把锁,提交任务、线程退出都要排队

JDK:一个 ctl,一次 CAScompareAndIncrementWorkerCount(c)

  • CAS 的期望值 c 里同时带着状态和线程数:状态一变,+1 就失败;线程数一变,进 TIDYING 的 CAS 也失败。两件事最多成功一件
  • 失败了就重读 ctl 再判断,不用拿锁
  • 代价:线程数上限从 2^31 − 1 降到 2^29 − 1。源码注释说真不够用时改成 AtomicLong 就行,现在用 int 更快更简单

决定胜负的约束:“加线程”和“关池子”必须互斥,又不想让每次提交、每次线程退出都去抢锁。把两样东西塞进一个 int,一次 CAS 就同时检查了两样。这个 CAS 只管名额;真正建线程、放进 workers,是之后在 mainLock 里做的,锁里还会再查一次状态,算第二道保险。

最少的 API

APIThreadPoolExecutor七个参数和常用方法
var pool = new ThreadPoolExecutor(
    2, 4,                         // corePoolSize, maximumPoolSize
    60, TimeUnit.SECONDS,         // keepAliveTime:线程数超过核心数时,闲多久回收
    new ArrayBlockingQueue<>(100),// workQueue:有界
    Executors.defaultThreadFactory(),
    new ThreadPoolExecutor.CallerRunsPolicy());
pool.execute(task);               // 没有返回值,异常会让线程退出
Future<?> f = pool.submit(task);   // 包成 FutureTask 再 execute,异常装进 Future
pool.shutdown();                  // 不收新任务,队列里的照跑
pool.awaitTermination(30, TimeUnit.SECONDS);
pool.close();                     // JDK 19 起:shutdown + 一直等到结束

异常怎么传、拒绝策略在关池后怎么表现、怎么监控、各版本改了什么,放到 第⑥步。

⑤亲眼看到:三个 if、插队、ctl 的位

核心 2、最大 4、队列容量 2,连续提交 8 个各跑 300 ms 的任务。每次提交后打印线程数和队列长度,再用反射读出 ctl 的 32 位。最后顺带验证 ① 里“核心数 0 只有 1 个线程”。

1TpePeek.java单文件,JDK 11+;读私有字段 ctl 要加 --add-opens
import java.lang.reflect.Field;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;

// 看 execute 的三步判断、任务的实际执行顺序、ctl 的位
// 运行:java --add-opens java.base/java.util.concurrent=ALL-UNNAMED TpePeek.java
public class TpePeek {
  static Field CTL;
  static String ctl(ThreadPoolExecutor p) throws Exception {
    int c = ((AtomicInteger) CTL.get(p)).get();
    String[] names = {"RUNNING", "SHUTDOWN", "STOP", "TIDYING", "TERMINATED"};
    int rs = c >> 29;                                   // 高 3 位
    String bits = String.format("%32s", Integer.toBinaryString(c)).replace(' ', '0');
    return bits.substring(0, 3) + " " + bits.substring(3) + "  →  " + names[rs + 1] + ",线程数 " + (c & ((1 << 29) - 1));
  }
  public static void main(String[] a) throws Exception {
    CTL = ThreadPoolExecutor.class.getDeclaredField("ctl"); CTL.setAccessible(true);
    var started = Collections.synchronizedList(new ArrayList<String>());
    var pool = new ThreadPoolExecutor(2, 4, 60, TimeUnit.SECONDS, new ArrayBlockingQueue<>(2));
    System.out.println("新建:        " + ctl(pool));
    for (int i = 1; i <= 8; i++) {
      final int id = i;
      String how;
      try {
        pool.execute(() -> { started.add("#" + id); sleep(300); });
        how = "线程 " + pool.getPoolSize() + ",队列 " + pool.getQueue().size();
      } catch (RejectedExecutionException e) { how = "被拒绝"; }
      System.out.printf("execute(#%d) → %s%n", i, how);
    }
    System.out.println("此刻:        " + ctl(pool));
    sleep(1000);
    System.out.println("开跑顺序:" + started);
    pool.shutdown();
    System.out.println("shutdown 后: " + ctl(pool));
    pool.awaitTermination(5, TimeUnit.SECONDS);
    System.out.println("全部结束:    " + ctl(pool));

    // core = 0 + 无界队列:只会有 1 个线程
    var zero = new ThreadPoolExecutor(0, 10, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<>());
    long t0 = System.nanoTime();
    for (int i = 0; i < 10; i++) zero.execute(() -> sleep(100));
    zero.shutdown(); zero.awaitTermination(10, TimeUnit.SECONDS);
    System.out.printf("core=0、max=10、无界队列:最多同时 %d 个线程,10 个 100 ms 的任务用了 %d ms%n", zero.getLargestPoolSize(), (System.nanoTime() - t0) / 1_000_000);
  }
  static void sleep(long ms) { try { Thread.sleep(ms); } catch (InterruptedException e) { } }
}
2运行JDK 21.0.12;JDK 11.0.32 只有“开跑顺序”里 #2 的位置不同
$ java --add-opens java.base/java.util.concurrent=ALL-UNNAMED TpePeek.java
新建:        111 00000000000000000000000000000  →  RUNNING,线程数 0
execute(#1) → 线程 1,队列 0        ← 第 ① 个 if:开核心线程
execute(#2) → 线程 2,队列 0
execute(#3) → 线程 2,队列 1        ← 第 ② 个 if:排队
execute(#4) → 线程 2,队列 2
execute(#5) → 线程 3,队列 2        ← 第 ③ 个 if:队列满了,开临时线程
execute(#6) → 线程 4,队列 2
execute(#7) → 被拒绝                ← 第 ③ 个 if 也失败:拒绝
execute(#8) → 被拒绝
此刻:        111 00000000000000000000000000100  →  RUNNING,线程数 4
开跑顺序:[#1, #2, #5, #6, #3, #4]     ← JDK 11 这次是 [#1, #5, #6, #2, #3, #4]
shutdown 后: 000 00000000000000000000000000100  →  SHUTDOWN,线程数 4
全部结束:    011 00000000000000000000000000000  →  TERMINATED,线程数 0
core=0、max=10、无界队列:最多同时 1 个线程,10 个 100 ms 的任务用了 1003 ms
看到了什么对应 ④
#5、#6 比早就在排队的 #3、#4 先开跑:临时线程的第一个任务不经过队列。#1、#2、#5、#6 之间谁先谁后,看线程启动快慢机制 1 第 ③ 个 if
高 3 位从 111(RUNNING)变成 000(SHUTDOWN)、011(TERMINATED),低 29 位就是线程数机制 2
核心数 0、无界队列:10 个任务排队一个个跑,1 秒多机制 1 第 ② 个 if 的复查
到这里已经能交差了:能画出 execute 的三个 if,说清临时线程为什么插队、ctl 的高 3 位和低 29 位各是什么,面试里“讲讲 execute 源码”这一问答得上。
已知还没处理的细节
  • 核心数 0 时,那 1 个线程是谁建的:⑥ 坑 1
  • 任务抛异常、关池以后的拒绝策略、shutdown 和 shutdownNow:⑥ 坑 2~4
  • addWorker、runWorker、getTask 逐行,以及自己当一回调度器:⑥ 第三圈

⑥细节:几件怪事的源码根据,和一行行读

旧页的“常踩的坑”讲的是配置(无界队列让最大线程数失效、拒绝了怎么办等),这里只放旧页没讲、要看源码才说得清的。

第一圈

注意事项

坑 1核心数 0 + 无界队列:永远只有 1 个线程← ④ 第 ② 个 if 的复查
现象
核心 0、最大 10、LinkedBlockingQueue:10 个 100 ms 的任务排着队一个个跑,用了 1003 ms,最多同时 1 个线程。实测
根源
第 ① 个 if 永远不成立(线程数不会 < 0),第 ② 个 if 永远能放进去,第 ③ 个 if 永远走不到。唯一建线程的地方是放进队列后的复查:“线程数是 0 就补一个”。补了 1 个之后,复查看到的就是 1,再也不补。
做法
想“空闲时缩到 0、忙时开到 10”,写成核心 10 + allowCoreThreadTimeOut(true):实测 10 个任务 117 ms 跑完,闲 1.5 秒后线程数回到 0。
坑 2execute 的任务抛异常:线程被换掉;submit 的:异常悄悄没了← ④ 工作线程的一生
现象
单线程池里一个任务抛异常,控制台打一行 Exception in thread "pool-1-thread-1",下一个任务跑在 pool-1-thread-2 上。换成 submit:线程名不变,什么也不打印,只有调 get() 才抛 ExecutionException。实测
根源
runWorker 不吞异常:调完 afterExecute 就原样抛出,线程走到 processWorkerExit,因为是异常退出,一定会补一个新线程。submit 先把任务包成 FutureTask,它自己 catch 住异常存起来,所以对线程池来说任务“正常结束”。
做法
任务里自己 try / catch;用 submit 就一定要处理返回的 Future;统一记日志可以覆写 afterExecute(submit 的任务在这里拿到的 Throwable 是 null,要从 Future 里取)。
坑 3CallerRunsPolicy 在池子关了以后,直接丢任务源码细节
现象
关池后提交 100 个任务:一个没跑,没有异常,没有日志。实测
根源
它的源码只有一句:if (!e.isShutdown()) r.run();。关了就什么也不做。默认的 AbortPolicy 关池后仍会抛 RejectedExecutionException。
做法
停机流程里先停止提交、再关池;或者自己写一个拒绝策略,关池时至少打日志。
坑 4shutdown 不停正在跑的,shutdownNow 也只是“打招呼”← ④ 工作线程的一生
现象
1 个线程、队列里 5 个任务:shutdown 后 6 个全跑完;shutdownNow 后正在 sleep 的那个被打断,退回 5 个没跑的。一个不看中断标志的忙循环,shutdownNow 之后 1 秒也没结束。实测
根源
shutdown 只中断“空闲”的线程(能 tryLock 到 Worker 的,就是没在跑任务的);shutdownNow 中断所有已启动的线程,可中断只是设一个标志,任务自己不检查就停不下来。
做法
任务里响应中断;关闭按 shutdown → awaitTermination → 超时再 shutdownNow 的顺序来。
坑 5忘了 shutdown:JVM 退不出,池子也不会被回收← ④ 核心线程停在 take 上
现象
main 里用 newFixedThreadPool(2) 跑一个任务就返回:进程不退出,6 秒后被 timeout 强行杀掉。实测
根源
核心线程空闲时停在 take() 上一直等,它们是非守护线程,JVM 不会退出;Worker 是内部类,持有池子的引用,活着的线程又是 GC Root,所以池子也回收不了。JDK 8 的 finalize() 会调 shutdown,但根本轮不到它执行;JDK 11 起它是空方法,JDK 27 删掉了。
做法
在 finally 里关;Spring 管理的池子交给容器关;JDK 19 起 ExecutorService 是 AutoCloseable,可以写 try-with-resources。
坑 6“核心线程”不是固定的那几个← ④ timed 是现算的
现象
核心 1、最大 2:先建的 T1 的任务先跑完时,10 次里留下的都是后建的 T2,T1 被回收了。实测
根源
getTask 每次都现算 timed = wc > corePoolSize:两个线程时谁进 getTask 都是“要限时”,谁先闲满 keepAlive 谁走。没有哪个 Worker 被标成核心。
做法
别在 ThreadLocal 里缓存“只给核心线程用”的东西,也别按线程名判断它是不是核心线程。
坑 7监控接口都要拿 mainLock← ④ workers + mainLock
现象
在请求路径上调 getActiveCount() 打日志,或者每 100 ms 采一次各种计数,高峰期提交变慢。
根源
getPoolSize、getActiveCount、getLargestPoolSize、getTaskCount、getCompletedTaskCount 都先拿 mainLock 再遍历 workers,和建线程、线程退出抢同一把锁。
做法
低频采样(比如 10 秒一次),别放在热路径上;队列长度 getQueue().size() 走的是队列自己的实现,不碰 mainLock。
第二圈

使用规范

自己 new ThreadPoolExecutor:有界队列 + 明确的拒绝策略

不用 Executors.newFixedThreadPool(无界队列)和 newCachedThreadPool(线程无上限)。

任务无限堆积、线程无限增长

核心数按真实并发设;要缩到 0 用 allowCoreThreadTimeOut

不要用“核心 0 + 无界队列”来省线程。

只剩 1 个线程干活(坑 1)

异常在任务里处理,或者 submit 后一定看 Future

统一兜底可以覆写 afterExecute。

线程被反复换掉、异常悄悄消失(坑 2)

给线程起名字

自定义 ThreadFactory,名字带上业务,如 order-notify-3。

jstack 里一堆 pool-7-thread-12,分不清是谁的

关闭三连,任务响应中断

shutdown → awaitTermination → shutdownNow;JDK 19 起可以 try-with-resources。

停机丢任务、进程退不出(坑 4、坑 5)

监控低频采样,别放热路径

线程数、活跃数、队列长度、拒绝次数,10 秒一次足够。

和建线程、线程退出抢 mainLock(坑 7)

第三圈

读源码:execute → addWorker → runWorker → getTask → processWorkerExit

executeThreadPoolExecutor.javaJDK 21u,行号和源码一致(OpenJDK 里按 GPLv2 分发;原作者 Doug Lea 把最初版本放入了公有领域)
1362        int c = ctl.get();1363        if (workerCountOf(c) < corePoolSize) {❶1364            if (addWorker(command, true))❷1365                return;1366            c = ctl.get();❸1367        }1368        if (isRunning(c) && workQueue.offer(command)) {❹1369            int recheck = ctl.get();❺1370            if (! isRunning(recheck) && remove(command))❻1371                reject(command);1372            else if (workerCountOf(recheck) == 0)❼1373                addWorker(null, false);1374        }1375        else if (!addWorker(command, false))❽1376            reject(command);
  • 1只读一次 ctl,拿到的是当时的快照,后面所有判断都可能已经过时,所以才有后面的重读和复查。
  • 2核心名额没满:把 command 当作新线程的 firstTask,成功就直接返回,任务根本不进队列。
  • 3addWorker 失败(名额被别人抢了,或者池子关了),重新读 ctl 再往下走。
  • 4还在 RUNNING 才放队列。offer 不阻塞,满了就返回 false。
  • 5放进去之后,再读一次 ctl:这就是“复查”。
  • 6这期间池子被关了:把任务撤回来再拒绝。remove 失败(已经被线程取走了)就不拒绝,任务照样会跑。
  • 7线程数是 0:补一个不带任务的线程,它会去队列里取。核心数 0 的池子,所有线程都是从这里来的。
  • 8队列放不进:按最大线程数再开一个,带上这个任务,这就是“插队”;开不了就拒绝。
addWorker先 CAS 占名额,再在锁里建线程
901    private boolean addWorker(Runnable firstTask, boolean core) {902        retry:903        for (int c = ctl.get();;) {904            // Check if queue empty only if necessary.905            if (runStateAtLeast(c, SHUTDOWN)❶906                && (runStateAtLeast(c, STOP)907                    || firstTask != null908                    || workQueue.isEmpty()))909                return false;910911            for (;;) {912                if (workerCountOf(c)❷913                    >= ((core ? corePoolSize : maximumPoolSize) & COUNT_MASK))914                    return false;915                if (compareAndIncrementWorkerCount(c))❸916                    break retry;917                c = ctl.get();  // Re-read ctl❹918                if (runStateAtLeast(c, SHUTDOWN))919                    continue retry;920                // else CAS failed due to workerCount change; retry inner loop921            }922        }923924        boolean workerStarted = false;925        boolean workerAdded = false;926        Worker w = null;927        try {928            w = new Worker(firstTask);❺929            final Thread t = w.thread;930            if (t != null) {931                final ReentrantLock mainLock = this.mainLock;932                mainLock.lock();❻933                try {934                    // Recheck while holding lock.935                    // Back out on ThreadFactory failure or if936                    // shut down before lock acquired.937                    int c = ctl.get();938939                    if (isRunning(c) ||❼940                        (runStateLessThan(c, STOP) && firstTask == null)) {941                        if (t.getState() != Thread.State.NEW)942                            throw new IllegalThreadStateException();943                        workers.add(w);❽944                        workerAdded = true;945                        int s = workers.size();946                        if (s > largestPoolSize)947                            largestPoolSize = s;948                    }949                } finally {950                    mainLock.unlock();951                }952                if (workerAdded) {953                    container.start(t);❾954                    workerStarted = true;955                }956            }957        } finally {958            if (! workerStarted)959                addWorkerFailed(w);❿960        }961        return workerStarted;
  • 1池子关了就不许加线程。唯一的例外:SHUTDOWN 时、不带任务、队列又不空,允许补一个线程把队列里剩下的跑完。
  • 2名额按 core 参数选核心数还是最大数。& COUNT_MASK 防止有人把它们设得比 2^29 − 1 还大。
  • 3CAS 把线程数 +1:期望值 c 里带着状态,状态变了也会失败。
  • 4失败了重读。状态变了回到最外层重新判断能不能加;只是线程数被别人改了,就在内层循环里重比名额。
  • 5名额占住了,才 new Worker;构造函数里用 ThreadFactory 建线程,可能失败(返回 null,或者内存不够抛异常)。
  • 6拿 mainLock,因为要动 workers 这个普通 HashSet。
  • 7锁里再查一次状态:拿锁的这段时间里池子可能被关了。
  • 8放进 workers,顺手更新 largestPoolSize。
  • 9出锁以后才启动线程。JDK 19 起通过 container.start(t) 启动,线程转储会按池子分组(见版本差异)。
  • 10没启动成功:addWorkerFailed 把 Worker 移出 workers、线程数 −1,再试着推进终止流程。
runWorker每跑一个任务,锁一次自己
1123    final void runWorker(Worker w) {1124        Thread wt = Thread.currentThread();1125        Runnable task = w.firstTask;1126        w.firstTask = null;❶1127        w.unlock(); // allow interrupts❷1128        boolean completedAbruptly = true;1129        try {1130            while (task != null || (task = getTask()) != null) {❸1131                w.lock();❹1132                // If pool is stopping, ensure thread is interrupted;1133                // if not, ensure thread is not interrupted.  This1134                // requires a recheck in second case to deal with1135                // shutdownNow race while clearing interrupt1136                if ((runStateAtLeast(ctl.get(), STOP) ||❺1137                     (Thread.interrupted() &&1138                      runStateAtLeast(ctl.get(), STOP))) &&1139                    !wt.isInterrupted())1140                    wt.interrupt();1141                try {1142                    beforeExecute(wt, task);❻1143                    try {1144                        task.run();1145                        afterExecute(task, null);1146                    } catch (Throwable ex) {1147                        afterExecute(task, ex);1148                        throw ex;❼1149                    }1150                } finally {1151                    task = null;1152                    w.completedTasks++;❽1153                    w.unlock();1154                }1155            }1156            completedAbruptly = false;❾1157        } finally {1158            processWorkerExit(w, completedAbruptly);1159        }1160    }
getTask取任务,顺便决定要不要退出
1042    private Runnable getTask() {1043        boolean timedOut = false; // Did the last poll() time out?10441045        for (;;) {1046            int c = ctl.get();10471048            // Check if queue empty only if necessary.1049            if (runStateAtLeast(c, SHUTDOWN)❶1050                && (runStateAtLeast(c, STOP) || workQueue.isEmpty())) {1051                decrementWorkerCount();1052                return null;1053            }10541055            int wc = workerCountOf(c);10561057            // Are workers subject to culling?1058            boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;❷10591060            if ((wc > maximumPoolSize || (timed && timedOut))❸1061                && (wc > 1 || workQueue.isEmpty())) {1062                if (compareAndDecrementWorkerCount(c))❹1063                    return null;1064                continue;1065            }10661067            try {1068                Runnable r = timed ?❺1069                    workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :1070                    workQueue.take();1071                if (r != null)1072                    return r;1073                timedOut = true;❻1074            } catch (InterruptedException retry) {1075                timedOut = false;❼1076            }1077        }1078    }
  • 1先拿走 firstTask,并把字段清空,免得一直引用着跑完的任务。
  • 2Worker 构造时 state 是 −1,这里 unlock 把它改成 0:从这一刻起才允许被中断。
  • 3主循环:手上有任务就跑,没有就去 getTask 取,取到 null 就退出循环。
  • 4跑任务前锁住自己。shutdown 用 tryLock 判断谁空闲,锁着的就不打断。
  • 5池子到了 STOP,确保线程被中断;否则清掉残留的中断标志,免得上一个任务的中断影响下一个。
  • 6beforeExecute / afterExecute 是留给子类的钩子。
  • 7任务抛异常:调完 afterExecute 就原样抛出去,循环结束。
  • 8不管成败,完成数 +1、解锁。
  • 9正常取到 null 退出时 completedAbruptly 是 false;因为异常退出时还是 true,processWorkerExit 就知道要补一个线程。
  • 1SHUTDOWN 且队列空、或者已经 STOP:线程数 −1,返回 null。
  • 2这次要不要限时等:允许核心超时,或者此刻线程数比核心数多。每次现算,所以没有“固定的核心线程”。
  • 3该退了(线程数超过最大数,或者限时等过一轮没等到),但如果自己是最后一个线程、队列又不空,就不退。
  • 4CAS 线程数 −1;失败说明 ctl 变了,回到循环开头重新判断。
  • 5限时就 poll(keepAliveTime),不限时就 take() 一直等。
  • 6poll 等满了还是 null:记下 timedOut,下一圈循环决定退不退。
  • 7被中断(shutdown、调小核心数时会中断空闲线程):不算超时,回去重新判断状态。
processWorkerExit退出前再看一眼要不要补人
997    private void processWorkerExit(Worker w, boolean completedAbruptly) {998        if (completedAbruptly) // If abrupt, then workerCount wasn't adjusted❶999            decrementWorkerCount();10001001        final ReentrantLock mainLock = this.mainLock;1002        mainLock.lock();1003        try {1004            completedTaskCount += w.completedTasks;1005            workers.remove(w);❷1006        } finally {1007            mainLock.unlock();1008        }10091010        tryTerminate();❸10111012        int c = ctl.get();1013        if (runStateLessThan(c, STOP)) {1014            if (!completedAbruptly) {1015                int min = allowCoreThreadTimeOut ? 0 : corePoolSize;❹1016                if (min == 0 && ! workQueue.isEmpty())❺1017                    min = 1;1018                if (workerCountOf(c) >= min)❻1019                    return; // replacement not needed1020            }1021            addWorker(null, false);❼1022        }1023    }
  • 1异常退出时 getTask 没来得及减线程数,这里补减。
  • 2拿 mainLock,把自己的完成数加到池子的总数里,再离开 workers。
  • 3每个线程退出都试着推进终止:如果池子关了、队列空了、自己是最后一个,就由它把状态推到 TERMINATED。
  • 4还该留几个线程:允许核心超时就是 0,否则是核心数。
  • 5就算该留 0 个,只要队列不空,至少留 1 个。这就是 ④ 机制 4 里“退出的一边”的那次检查。
  • 6线程数够了就不补。
  • 7否则补一个不带任务的线程。异常退出时一律补,所以抛异常的线程会被“换掉”。

实验台:你来当线程调度器 先猜,再一步步点

提交线程(S)、工作线程(W)、调用 shutdown 的线程(X)都按上面的源码拆成原子步骤,谁先走由你决定。日志前的行号对应源码。停在 poll(keepAlive) 上的线程可以点“等满 keepAlive”让它超时。

先在上面选一个猜测,再开始点。

同一套模拟逻辑在 node 里随机交错测过:JDK 的写法随机 3 万次、专门针对“最后一个线程正要走”的 5 万次,没有一次任务卡在没人取的队列里;去掉复查,核心数 0 时 5 万次全部卡住;只去掉退出前的检查,5 万次里卡住 2 次。概率很小,但会发生。

版本差异

JDK改了什么影响
8finalize() 会调 shutdown();常量叫 CAPACITY有线程活着时池子根本不会被回收,这个 finalize 基本派不上用场
11finalize() 变成空方法(JDK-8190324);CAPACITY 改名 COUNT_MASK,addWorker 的条件改写成更易读的形式行为不变;更不能指望“没人引用就自动关”
13addWorker 检查线程能不能启动,从 t.isAlive() 改成 t.getState() != NEW(JDK-8221892)ThreadFactory 返回了已经启动过的线程时,报错更准确
19线程通过 SharedThreadContainer 启动(虚拟线程那批改动,JDK-8284161);ExecutorService 加了 close()、成了 AutoCloseablejcmd <pid> Thread.dump_to_file -format=json 里,池子的线程归在名为 java.util.concurrent.ThreadPoolExecutor@… 的容器下(本机 JDK 21 实测);可以 try-with-resources
24Security Manager 永久禁用(JEP 486),shutdown 里的权限检查删掉无
25构造参数不合法时,IllegalArgumentException 带上了说明(JDK-8347491)报错更好懂
27删除空的 finalize()(JDK-8371748)子类里重写 finalize 并调 super 的代码要改

对照了 jdk8u、jdk11u、jdk17u、jdk21u、jdk25u 和主线的源码;每条改动属于哪个版本,是用 GitHub 上主线的提交和各版本的 GA 标签比出来的。从 JDK 8 到主线,execute 的三个 if 一个字没变。

再往深一点

Worker 为什么自己写一把不可重入的锁

  • 源码注释:锁住正在跑任务的线程,是为了让“叫醒空闲线程”的中断不会打到正在跑的任务上。shutdown、调小核心数都会 tryLock 每个 Worker,锁得上的才中断。
  • 不用 ReentrantLock:任务里如果调了 setCorePoolSize,它会去中断空闲线程,可重入的锁会让当前线程 tryLock 到自己,把自己中断掉。
  • 初始 state 是 −1:线程还没真正开始跑任务前,谁也 tryLock 不到它,也就不会被中断。

workers 为什么用 HashSet + 锁,不用并发集合

  • 源码注释给的头号理由:锁让 interruptIdleWorkers 串行执行,避免关池时一大批正在退出的线程同时去中断别人,形成“中断风暴”。
  • largestPoolSize、completedTaskCount 这类统计也顺便有了锁保护。
  • 代价就是坑 7:监控接口也要拿这把锁。

终止是“一个传一个”完成的

  • tryTerminate 发现还有线程,只中断一个空闲线程(ONLY_ONE)。那个线程醒来、退出,又调 tryTerminate,再中断下一个。
  • 最后一个线程退出时线程数是 0,它用 CAS 把状态从 SHUTDOWN 推到 TIDYING,调 terminated() 钩子,再设成 TERMINATED,唤醒 awaitTermination。
  • 这个 CAS 和 addWorker 的 +1 互斥:要么终止成功,要么加线程成功。

先占名额,后建线程

  • addWorker 先 CAS 线程数 +1,再 new Worker、建线程、启动。顺序反过来的话,几个提交者可能同时建出线程,再发现名额不够,白建一场。
  • 建线程可能失败(ThreadFactory 返回 null,或者 unable to create native thread),所以要有 addWorkerFailed 回滚这个名额。
  • 名额的检查和占用一次 CAS 完成,不拿锁;只有改 workers 才拿 mainLock。

面试官会怎么问

讲讲 execute 的源码。

读一次 ctl:线程数小于核心数就 addWorker(command, true),任务作为新线程的第一个任务直接跑。否则如果池子在跑,就 offer 进队列;放进去后复查:池子关了就撤回并拒绝,线程数为 0 就补一个不带任务的线程。放不进队列就 addWorker(command, false) 开临时线程,再失败就拒绝。

ctl 是什么?为什么把两个值放一起?

一个 AtomicInteger,高 3 位是运行状态,低 29 位是线程数。放在一起,“状态还允许、名额也够才 +1”和“已经关闭、线程数为 0 才进入 TIDYING”都能用一次 CAS 完成,两者天然互斥,不用拿锁。

放进队列后为什么还要再检查一次?

两种情况:一是放进去前后池子被关了,要撤回任务;二是这期间最后一个线程退出了,或者核心数本来就是 0,队列里的任务没人取,所以线程数为 0 时补一个。线程退出时也会检查“队列不空却没线程”,两边各查一次,保证任务不会卡在队列里。

线程池里的线程怎么复用、怎么回收?

每个 Worker 的线程跑 runWorker 循环:先跑 firstTask,然后不断 getTask。线程数不超过核心数时用 take() 一直等,超过时用 poll(keepAliveTime) 限时等;限时等没等到、线程数仍超过核心数(或允许核心超时),就 CAS 减线程数并退出。所以“核心线程”不是固定的哪几个。

任务抛异常会怎样?execute 和 submit 有什么区别?

execute:异常从 runWorker 抛出,线程退出,processWorkerExit 补一个新线程,默认的未捕获异常处理器会打印堆栈。submit:任务被包成 FutureTask,异常存在 Future 里,线程不退出,不调 get() 就永远看不到。

下一圈

  • 已做:线程池下一圈:怎么配、拒绝策略、动态调参与监控
  • AQS:Worker、ReentrantLock、CountDownLatch 共用的那套 state + CLH 队列(已写成下一页)
  • FutureTask 的状态机:cancel(true) 怎么中断、get() 的等待链表
  • ScheduledThreadPoolExecutor:延迟队列 DelayedWorkQueue 和固定频率任务的“漂移”
  • ForkJoinPool:工作窃取、ManagedBlocker 补偿线程,和 CompletableFuture 的默认池
  • 动态调参:运行中改 setCorePoolSize、setMaximumPoolSize 时,源码里发生了什么
  • 虚拟线程:为什么不该把虚拟线程放进池子,newVirtualThreadPerTaskExecutor 怎么限流

↺回到第⑤步:改 TpePeek

每个练习都写了做对时会看到什么。

练习 1 · 换掉核心数 0 的写法

把 TpePeek 末尾那个池子改成核心 10、最大 10、keepAlive 1 秒,再调 allowCoreThreadTimeOut(true),跑完任务后等 1.5 秒看 getPoolSize()。

做对了会看到:10 个 100 ms 的任务一百多毫秒就跑完(本机 113~117 ms),1.5 秒后线程数回到 0;再提交一个任务,又从 0 建出 1 个线程。

用到:坑 1,getTask 的 timed

练习 2 · 让异常现形

继承 ThreadPoolExecutor 覆写 afterExecute(r, t) 打印 t;分别用 execute 和 submit 提交一个抛异常的任务,打印每个任务所在的线程名。

做对了会看到:execute 时 t 是那个异常,下一个任务换到了新线程;submit 时 t 是 null,线程名不变,要从 ((Future<?>) r).get() 里才拿得到异常。

用到:坑 2,runWorker 第 7、9 条

练习 3 · 看看线程转储里的池子

JDK 21 上让 TpePeek 的池子跑着长任务,另开终端执行 jcmd <pid> Thread.dump_to_file -format=json dump.json,找 threadContainers。

做对了会看到:一个名为 java.util.concurrent.ThreadPoolExecutor@… 的容器,里面正好是池子的几个线程;JDK 17 上没有这个命令。

用到:版本差异 JDK 19

1. 用一句话说出 execute 的精华。

三个 if:没满核心就开线程直接跑,否则放队列并复查,放不进就开临时线程或拒绝;状态和线程数在一个 int 里,一次 CAS 同时检查两样。

← 开头一段

2. 不用线程池、每个任务 new Thread,痛在哪?

建、销毁线程都进内核,每次都付这笔钱;突发流量时线程数没有上限,内存和调度都会被拖垮。

← ②

3. 核心数 0 + 无界队列只有 1 个线程,根源是哪条?

execute 第 ② 个 if 的复查:只有线程数为 0 时才补线程。第 ① 个 if 不成立,第 ③ 个 if 永远走不到。

← ④ 机制 1

4. 预测:核心 2、最大 4、SynchronousQueue,连续提交 3 个长任务,会有几个线程?

3 个。前 2 个走第 ① 个 if;第 3 个时 SynchronousQueue 的 offer 只有在有线程正停在 poll 上才成功,两个核心线程都在忙,offer 失败,走第 ③ 个 if 开临时线程。

← ④ 机制 1;SynchronousQueue 见上一页

5. 预测:核心 2,线程 A 在跑一个很长的任务,线程 B 空闲停在 take() 上。这时提交第 3 个任务,它怎么到 B 手里?

线程数 2 不小于核心数,第 ① 个 if 不成立;任务被 offer 进队列,正停在 take() 上的 B 被唤醒取走。不会“直接交给空闲线程”,一定经过队列。

← ④ 机制 1 + 机制 3

6. 预测:allowCoreThreadTimeOut(true),池子闲到线程数 0 以后,再提交一个任务,走的是哪个 if?

第 ① 个:线程数 0 小于核心数,直接 addWorker(task, true),任务作为第一个任务跑。本机练习 1 里正是从 0 建回 1 个线程。

← ④ 机制 1

7. 预测:如果 runWorker 把任务的异常 catch 住、不往外抛,会有什么变化?

线程不会因为异常退出,也就不会被换掉,ThreadLocal 里的东西保得住;但默认的未捕获异常处理器也不再打印堆栈,execute 提交的任务出错就和 submit 一样悄无声息,除非 afterExecute 里记日志。

← ④ 机制 3 + 坑 2