承接的问题
卷一第 10 章讲的是线程池的用法:七个参数怎么配、execute 的三步流程、拒绝策略怎么选。那部分是外部视角,读者知道任务被拒绝或被执行,但看不到池内部如何判断"该不该再开一个线程"。本节只回答三个问题:
runState(运行状态)和workerCount(工作线程数)为什么被塞进同一个int?- 五个状态里
RUNNING是唯一的负数,这个取值是随手选的还是必须的? Worker只是跑任务的线程包装,为什么要继承AbstractQueuedSynchronizer?
答案合起来就是线程池的控制面:用一个原子变量同时表达"池能不能接活"和"池里有几个线程",用数值顺序表达状态机,用一把不可重入的内置锁表达"这个线程此刻是否空闲"。
一个 int 装两个字段
ctl 的类型是 AtomicInteger,不是 AtomicLong——本机 javap -p java.util.concurrent.ThreadPoolExecutor 的输出里这一行是 private final java.util.concurrent.atomic.AtomicInteger ctl;。源码里的定义与拆包:
// JDK 25 源码摘录,已删减
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
private static final int COUNT_BITS = Integer.SIZE - 3;
private static final int COUNT_MASK = (1 << COUNT_BITS) - 1;
// runState is stored in the high-order bits
private static final int RUNNING = -1 << COUNT_BITS;
private static final int SHUTDOWN = 0 << COUNT_BITS;
private static final int STOP = 1 << COUNT_BITS;
private static final int TIDYING = 2 << COUNT_BITS;
private static final int TERMINATED = 3 << COUNT_BITS;
// Packing and unpacking ctl
private static int runStateOf(int c) { return c & ~COUNT_MASK; }
private static int workerCountOf(int c) { return c & COUNT_MASK; }
private static int ctlOf(int rs, int wc) { return rs | wc; }
COUNT_BITS = 32 - 3 = 29,COUNT_MASK = 0x1FFFFFFF,位布局如下:
| 位区间 | 宽度 | 含义 | 掩码 |
|---|---|---|---|
| 31–29 | 3 | runState | 0xE0000000(~COUNT_MASK) |
| 28–0 | 29 | workerCount | 0x1FFFFFFF |
因为 workerCount 永远非负且不超过 COUNT_MASK,加减一不会向上进位,所以 compareAndIncrementWorkerCount 直接 ctl.compareAndSet(expect, expect + 1),不需要拆包再拼;decrementWorkerCount() 用 ctl.addAndGet(-1) 也安全——只要计数大于 0,借位不会越过第 29 位污染状态。
不碰反射,先按同样的常量合成五个状态,确认数值顺序(本机输出):
public final class CtlBits {
static final int COUNT_BITS = Integer.SIZE - 3;
static final int COUNT_MASK = (1 << COUNT_BITS) - 1;
static final int RUNNING = -1 << COUNT_BITS;
static final int SHUTDOWN = 0 << COUNT_BITS;
static final int STOP = 1 << COUNT_BITS;
static final int TIDYING = 2 << COUNT_BITS;
static final int TERMINATED = 3 << COUNT_BITS;
static String bits(int c) {
String s = String.format("%32s", Integer.toBinaryString(c)).replace(' ', '0');
return s.substring(0, 3) + "_" + s.substring(3);
}
public static void main(String[] args) {
int[][] rows = {{RUNNING, 0}, {RUNNING, 4}, {SHUTDOWN, 4},
{STOP, 2}, {TIDYING, 0}, {TERMINATED, 0},
{RUNNING, COUNT_MASK}};
String[] names = {"RUNNING", "RUNNING", "SHUTDOWN", "STOP", "TIDYING", "TERMINATED", "RUNNING"};
for (int i = 0; i < rows.length; i++) {
int c = rows[i][0] | rows[i][1];
System.out.printf("%-10s wc=%-9d ctl=%-11d 0x%08X %s isRunning=%b%n",
names[i], rows[i][1], c, c, bits(c), c < SHUTDOWN);
}
}
}
完整输出:
RUNNING wc=0 ctl=-536870912 0xE0000000 111_00000000000000000000000000000 isRunning=true
RUNNING wc=4 ctl=-536870908 0xE0000004 111_00000000000000000000000000100 isRunning=true
SHUTDOWN wc=4 ctl=4 0x00000004 000_00000000000000000000000000100 isRunning=false
STOP wc=2 ctl=536870914 0x20000002 001_00000000000000000000000000010 isRunning=false
TIDYING wc=0 ctl=1073741824 0x40000000 010_00000000000000000000000000000 isRunning=false
TERMINATED wc=0 ctl=1610612736 0x60000000 011_00000000000000000000000000000 isRunning=false
RUNNING wc=536870911 ctl=-1 0xFFFFFFFF 111_11111111111111111111111111111 isRunning=true
最后一行值得记住:RUNNING 加上满员计数 ctl 恰好是 -1。这说明在 29 位计数的整个取值范围内,"ctl 为负"与"池处于 RUNNING“等价,isRunning 才敢写成 return c < SHUTDOWN;——一次与零比较,不需要掩码。
为什么不是两个字段
拆成 volatile int runState 加 AtomicInteger workerCount 看起来更清晰,但会在三处出问题。
第一处是 tryTerminate:它要回答"队列空了、线程也归零了吗”,这个判断必须和状态读在同一时刻:
// JDK 25 源码摘录,已删减
if (isRunning(c) ||
runStateAtLeast(c, TIDYING) ||
(runStateLessThan(c, STOP) && ! workQueue.isEmpty()))
return;
if (workerCountOf(c) != 0) { // Eligible to terminate
interruptIdleWorkers(ONLY_ONE);
return;
}
两个变量的话,读完状态再读计数,中间可能插进一次 shutdown() 或一次 worker 退出,判断就建立在两个不同时刻的快照上。一个 ctl.get() 读到的是一致的一对值。
第二处是状态转移必须保留计数。advanceRunState 读到 c、算出新 ctl、CAS 回去,失败就重读:
// JDK 25 源码摘录,已删减
private void advanceRunState(int targetState) {
for (;;) {
int c = ctl.get();
if (runStateAtLeast(c, targetState) ||
ctl.compareAndSet(c, ctlOf(targetState, workerCountOf(c))))
break;
}
}
CAS 的期望值是整个 ctl,所以"把状态改成 STOP"和"期间计数有变化"不会互相覆盖:计数变了,CAS 失败,重读后再拼一次。源码注释还留了口子:workerCount 被限制在 2^29-1(约五亿),“如果将来不够用,可以把变量改成 AtomicLong 并调整移位常量,但在需要之前,用 int 更快也更简单”。热路径上这一点很实在:addWorker 的核心动作就是一次 CAS,拆成两个变量要么加锁、要么两次 CAS 并处理中间态。
RUNNING 为什么是负数
五个常量是 -1 << 29、0 << 29、1 << 29、2 << 29、3 << 29,按有符号 int 排序恰好是 RUNNING(-536870912) < SHUTDOWN(0) < STOP(536870912) < TIDYING(1073741824) < TERMINATED(1610612736)。
源码注释对此有一句关键的话:“The numerical order among these values matters, to allow ordered comparisons. The runState monotonically increases over time, but need not hit each state.” 三件事因此变得很便宜:
// JDK 25 源码摘录,已删减
private static boolean runStateLessThan(int c, int s) { return c < s; }
private static boolean runStateAtLeast(int c, int s) { return c >= s; }
private static boolean isRunning(int c) { return c < SHUTDOWN; }
- “至少到某个状态"一次比较就够。
addWorker判断池不再接新任务用runStateAtLeast(c, SHUTDOWN),getTask判断要不要直接退出用runStateAtLeast(c, STOP)。换成枚举,这里要么两次比较,要么一次ordinal()。 - “状态只会往前走"由数值单调保证。 回退不可能发生,因为
advanceRunState的目标值总不小于当前值,且 CAS 只在c未变时成功。 isRunning退化成c < 0。RUNNING编码成111xxxxx,让所有运行中的ctl都是负数,与SHUTDOWN的0分界清晰,execute热路径用最便宜的一次判断挡掉"池已关闭”。
RUNNING 取 -1 << 29 而不是别的值,本质是让符号位的语义和状态序一致:最高三位全 1 时数值最负,正好对应状态序最小;同时空出向下的编码空间,将来若要在 RUNNING 之前加状态也不必改动既有常量。
五个状态与单向转移
状态机只有五条边,源码注释列全了:
| 转移 | 触发条件 | 触发点 |
|---|---|---|
RUNNING → SHUTDOWN | 调用 shutdown() | advanceRunState(SHUTDOWN) |
RUNNING 或 SHUTDOWN → STOP | 调用 shutdownNow() | advanceRunState(STOP) |
SHUTDOWN → TIDYING | 队列为空且 workerCount == 0 | tryTerminate() |
STOP → TIDYING | workerCount == 0(不看队列) | tryTerminate() |
TIDYING → TERMINATED | terminated() 钩子执行完毕 | tryTerminate() 最后一步 |
shutdown()不会走到STOP。 它只把状态推到SHUTDOWN,已排队的任务继续执行;只有shutdownNow()才推到STOP并中断正在跑的任务。SHUTDOWN → TIDYING要"队列空"和"计数零"同时成立。 队列空但还有 worker 在跑时,tryTerminate用interruptIdleWorkers(ONLY_ONE)叫醒一个空闲线程,让它自己重新检查状态并退出,而不是原地等。TIDYING只停一瞬间。 进入TIDYING的线程先跑terminated()钩子,再在finally里ctl.set(ctlOf(TERMINATED, 0)),所以外部几乎观察不到TIDYING,能稳定看到的是TERMINATED。
用反射看真实线程池的 ctl
常量是算出来的,真实池的 ctl 才是可信的。下面这段程序反射读私有字段,在几个关键时点各拍一张快照。--add-opens 只是实验手段,不进正文代码:
import java.lang.reflect.Field;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
/** 读取运行中线程池的 ctl。仅用于观察,不要在生产代码里这么做。 */
public final class CtlProbe {
static final Field CTL;
static {
try {
CTL = ThreadPoolExecutor.class.getDeclaredField("ctl");
CTL.setAccessible(true);
} catch (ReflectiveOperationException e) { throw new ExceptionInInitializerError(e); }
}
static int ctlOf(ThreadPoolExecutor p) throws Exception {
return ((AtomicInteger) CTL.get(p)).get();
}
static void dump(String label, ThreadPoolExecutor p) throws Exception {
int c = ctlOf(p);
System.out.printf("%-32s ctl=%-12d 0x%08X rs=%-11d wc=%d poolSize=%d%n",
label, c, c, c & 0xE0000000, c & 0x1FFFFFFF, p.getPoolSize());
}
public static void main(String[] args) throws Exception {
ThreadPoolExecutor p = new ThreadPoolExecutor(2, 6, 30, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(2), Thread.ofPlatform()::unstarted);
dump("新建后", p);
CountDownLatch hold = new CountDownLatch(1);
for (int i = 0; i < 2; i++) p.execute(() -> await(hold));
Thread.sleep(200);
dump("2 个核心线程在跑", p);
for (int i = 0; i < 2; i++) p.execute(() -> await(hold));
Thread.sleep(100);
dump("队列塞满但未超 core", p);
for (int i = 0; i < 2; i++) p.execute(() -> await(hold));
Thread.sleep(200);
dump("队列满后扩容到 4", p);
p.shutdown();
dump("shutdown() 之后", p);
hold.countDown();
p.awaitTermination(5, TimeUnit.SECONDS);
dump("terminated", p);
}
static void await(CountDownLatch l) {
try { l.await(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
}
}
javac --release 25 -d out CtlProbe.java && java --add-opens java.base/java.util.concurrent=ALL-UNNAMED -cp out CtlProbe
本机输出:
新建后 ctl=-536870912 0xE0000000 rs=-536870912 wc=0 poolSize=0
2 个核心线程在跑 ctl=-536870910 0xE0000002 rs=-536870912 wc=2 poolSize=2
队列塞满但未超 core ctl=-536870910 0xE0000002 rs=-536870912 wc=2 poolSize=2
队列满后扩容到 4 ctl=-536870908 0xE0000004 rs=-536870912 wc=4 poolSize=4
shutdown() 之后 ctl=4 0x00000004 rs=0 wc=4 poolSize=4
terminated ctl=1610612736 0x60000000 rs=1610612736 wc=0 poolSize=0
- 第三行
ctl完全没变。队列满时没超核心线程数,任务只是排队,workerCount保持 2——这正是execute第二步"入队成功就返回"的体现。 shutdown()之后rs=0而wc=4仍是 4。同一个int里,状态和计数各自独立变化,这一行把"高 3 位 / 低 29 位"讲得比任何图都清楚。terminated时ctl=1610612736(0x60000000),正是3 << 29。
Worker 为什么要继承 AQS
Worker 继承 AbstractQueuedSynchronizer,但只用了一把最简的、不可重入的互斥锁:
// JDK 25 源码摘录,已删减
private final class Worker
extends AbstractQueuedSynchronizer
implements Runnable
{
final Thread thread;
Runnable firstTask;
volatile long completedTasks;
Worker(Runnable firstTask) {
setState(-1); // inhibit interrupts until runWorker
this.firstTask = firstTask;
this.thread = getThreadFactory().newThread(this);
}
public void run() {
runWorker(this);
}
protected boolean tryAcquire(int unused) {
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
protected boolean tryRelease(int unused) {
setExclusiveOwnerThread(null);
setState(0);
return true;
}
public void lock() { acquire(1); }
public boolean tryLock() { return tryAcquire(1); }
public void unlock() { release(1); }
}
锁的语义是反过来的:不是"拿到锁才能跑任务”,而是"跑任务期间一直持有锁"。 runWorker 在 task.run() 之前 w.lock(),跑完在 finally 里 w.unlock()。于是 state == 1 等价于"这个 worker 正在执行任务",tryAcquire 成功等价于"这个 worker 此刻空闲、正在等任务"。
这个等价关系是关闭流程的基础。interruptIdleWorkers 只中断"能抢到锁"的 worker:
// JDK 25 源码摘录,已删减
for (Worker w : workers) {
Thread t = w.thread;
if (!t.isInterrupted() && w.tryLock()) {
try {
t.interrupt();
} finally {
w.unlock();
}
}
if (onlyOne)
break;
}
正在跑任务的 worker 锁在自己手里,tryLock 返回 false,不会被中断;空闲等任务的 worker 没持锁,tryLock 成功,被 interrupt 唤醒后从 workQueue.take() 抛 InterruptedException,回到 getTask 循环重新判断状态。“是否空闲"因此不需要额外的布尔字段,也不需要和任务执行做二次同步——锁状态本身就是答案。
不用 ReentrantLock 的原因,源码注释写得很直白:“we do not want worker tasks to be able to reacquire the lock when they invoke pool control methods like setCorePoolSize”。假设任务内部调用 setCorePoolSize,会走到 interruptIdleWorkers;如果锁可重入,当前 worker 的 tryLock 会成功,然后 interrupt() 自己,把正在执行的任务打断。不可重入让 state == 1 时 tryAcquire 必然失败,这一刀自动避开。
构造函数里的 setState(-1) 是另一个细节。interruptIfStarted 只在 getState() >= 0 时中断,初值 -1 表示"线程还没真正开始跑”,中断被抑制;runWorker 第一件事是 w.unlock() 把状态清回 0,中断才被允许。这堵住了"worker 刚创建、还没进入 runWorker 就被 shutdownNow 中断"的窗口。
在 libraryd-conc 里落地
PooledLoanService(卷一第 10 章)把借阅请求交给一个 ThreadPoolExecutor。ctl 是私有的,生产代码不应该反射它,但可以按同样的口径做一个只读快照,用于健康检查和告警:
package com.plume.conc.core;
import java.util.concurrent.BlockingQueue;
/** 借阅池的只读快照。各字段独立读取,不构成同一瞬间的一致视图。 */
public record PoolSnapshot(int poolSize, int activeCount, int queueSize, int remainingCapacity) {
public boolean saturated() {
return remainingCapacity == 0 && activeCount == poolSize;
}
}
// 在 PooledLoanService 中新增
public PoolSnapshot snapshot() {
BlockingQueue<Runnable> q = pool.getQueue();
return new PoolSnapshot(pool.getPoolSize(), pool.getActiveCount(),
q.size(), q.remainingCapacity());
}
getActiveCount() 的 javadoc 明确写了是近似值,getCompletedTaskCount() 也是"大约完成的任务数"。这几个数分别来自 workers 集合、completedTaskCount 字段和队列自身的锁,读的时刻不同,拼在一起只能当趋势看。真正的判断逻辑要留余量:saturated() 为真意味着"队列满了且所有线程都在忙",这时 submitAndWait 里的拒绝开始计数——拒绝率比瞬时队列长度更能说明问题,这是卷一第 10 章拒绝策略那套指标的自然延伸。
这个设计的代价
第一,2^29 的上限是硬编码的。 maximumPoolSize 会被 & COUNT_MASK,超过 536870911 的部分直接被截断,new ThreadPoolExecutor(…, Integer.MAX_VALUE, …) 实际上是 536870911。工程上开不到这么大,但配置校验时要知道这个上限存在。
第二,状态与计数共享一个字,所有变更互相竞争。 addWorker 的计数 CAS、shutdown 的状态 CAS、getTask 里超时退出的计数 CAS,全部落在同一个 AtomicInteger 上。线程数频繁起落的池会出现 CAS 重试,这是合并字段的代价;它换来的是一致快照,在低频的控制面上划算,在高频的数据面上不划算——这也是 LongAdder 反而要把计数拆开的原因。
第三,workerCount 不等于"活着的线程数"。 源码注释说它表示"已允许启动、且还没被允许停止"的线程数,在工厂创建线程失败、线程退出但还在收尾记账时,它会和真实线程数短暂不一致。getPoolSize() 读的是 workers 集合大小,两者也不保证相等:做容量规划用 getPoolSize(),做状态判断用 ctl,不要混。
第四,不要把这套位压缩照搬到业务代码。 它成立的前提很苛刻:两个字段取值范围固定、可以牺牲计数上限、状态序与数值序一致、所有更新走同一个 CAS。业务对象很少有这些性质,硬套只会得到一堆难调试的魔数。
最后,不要用反射读 ctl 做监控。 它依赖 --add-opens,将来可能被模块系统挡住,读到的是内部表示而非契约。要观测就用 getPoolSize / getActiveCount / getQueue(),接受它们不是同一瞬间的快照。
小结
ctl 用一个 int 同时表达池的状态和线程数:高 3 位 runState、低 29 位 workerCount,掩码保证计数加减不进位,热路径只需一次 CAS。RUNNING 取 -1 << 29 让数值序与状态序一致,runStateAtLeast、isRunning 退化成整数比较。Worker 继承 AQS 获得一把不可重入的锁,用"是否持锁"表达"是否正在跑任务",中断因此只落到空闲线程身上。
阅读导航:上一节 10.3 无锁 vs 有锁实测 | 下一节 11.2 addWorker、runWorker 与退出
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。