在 Java 线程池 ThreadPoolExecutor 的实现中,ctl 是一个十分关键的字段。它的类型是 AtomicInteger,但内部却用位运算同时承载了两个维度的信息:线程池运行状态和有效线程数。这个设计让一次 CAS 操作就能同时修改状态和数量,避免了为两个字段分别加锁或多次原子操作带来的不一致问题。要理解 ctl 是如何做到这一点的,需要从二进制位布局和对应的位运算方法入手。

线程池状态与线程数的二进制布局
ThreadPoolExecutor 把一个 int 类型的 32 位二进制数据分成了两部分:高 3 位用来表示运行状态,低 29 位用来表示工作线程数量。之所以选择 3 位给状态,是因为线程池一共定义了 5 种运行状态,恰好需要 3 个二进制位才能完整区分。COUNT_BITS 的值为 Integer.SIZE - 3,也就是 29,而 CAPACITY 则等于 (1 << COUNT_BITS) - 1,即 2 的 29 次方减 1,换算成十进制是 536870911。这个值就是低 29 位全部为 1 的掩码,用来提取或限制线程数。
五个运行状态对应的常量值如下:RUNNING 是 -1 左移 29 位,二进制为 11100000 00000000 00000000 00000000;SHUTDOWN 是 0,二进制为 00000000 00000000 00000000 00000000;STOP 是 1 左移 29 位,二进制为 00100000 00000000 00000000 00000000;TIDYING 是 2 左移 29 位,二进制为 01000000 00000000 00000000 00000000;TERMINATED 是 3 左移 29 位,二进制为 01100000 00000000 00000000 00000000。这些状态值的高 3 位从 111 到 011 依次变化,而低 29 位全部是 0,不会与线程数部分产生干扰。
下面的代码展示了这些常量的定义。注意其中 << 是左移运算符,-1 在 Java 中按补码表示为全 1,左移 29 位后低 29 位会自动补 0,所以 RUNNING 的高 3 位是 111,其余位是 0。SHUTDOWN 直接就是 0,因此它的状态值最小但含义是停止接收新任务。
private static final int COUNT_BITS = Integer.SIZE - 3; private static final int CAPACITY = (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;
位运算拆分与组合的核心方法
ThreadPoolExecutor 提供了三个私有静态方法来拆分和组合 ctl 的值:runStateOf(int c) 返回线程池状态,workerCountOf(int c) 返回有效线程数,ctlOf(int rs, int wc) 则把状态和线程数合并成一个 int。runStateOf 的实现是 c & ~CAPACITY,由于 CAPACITY 的低 29 位全是 1,取反后 ~CAPACITY 的低 29 位变成 0,高 3 位变成 1,再和 c 做按位与运算,就只保留了高 3 位的状态信息。workerCountOf 的实现是 c & CAPACITY,与运算会保留低 29 位,丢弃高 3 位,从而得到线程数。ctlOf 的实现是 rs | wc,因为状态值的高 3 位和线程数的低 29 位分别占据不同区间,按位或运算可以直接把它们拼在一起。
假设某个时刻 ctl 的二进制值是 11100000 00000000 00000000 00001010,那么高 3 位 111 表示 RUNNING 状态,低 29 位转换成十进制是 10,说明当前有 10 个工作线程。runStateOf 会先对 CAPACITY 取反得到 11100000 00000000 00000000 00000000,再做与运算得到 11100000 00000000 00000000 00000000,也就是 RUNNING 常量值。workerCountOf 则做与运算得到 00000000 00000000 00000000 00001010,即 10。反过来,如果要组合 RUNNING 和 10,就执行 RUNNING | 10,结果和原来的 ctl 完全一致。
除了这三个基础方法,线程池还封装了一系列状态判断方法。例如 isRunning(int c) 的实现是 c < SHUTDOWN,因为 SHUTDOWN 的数值是 0,而 RUNNING 是负数,其他状态都是非负数,所以只要 c 小于 0 就说明仍然处于 RUNNING 状态。isShutdown 则是 !isRunning(c),也就是 c 大于等于 0。这些判断都不需要先拆分状态,直接利用状态值的大小关系就能快速完成,进一步减少了计算开销。下面的代码是这些方法在源码中的典型写法。
private static int runStateOf(int c) { return c & ~CAPACITY; }
private static int workerCountOf(int c) { return c & CAPACITY; }
private static int ctlOf(int rs, int wc) { return rs | wc; }
private static boolean isRunning(int c) {
return c < SHUTDOWN;
}
ctl 在源码中的并发控制体现
ctl 被声明为 private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0)); 初始状态是 RUNNING 且线程数为 0。由于 AtomicInteger 内部使用 volatile 修饰 value,并且提供 compareAndSet 等原子操作,线程池可以在不加锁的情况下安全地更新状态和线程数。在 addWorker 方法中,当需要增加一个工作线程时,会调用 compareAndIncrementWorkerCount,这个方法内部执行的是 ctl.compareAndSet(expect, expect + 1)。因为线程数存储在低 29 位,直接加 1 不会影响高 3 位的状态,除非线程数已经达到 CAPACITY 导致溢出。
addWorker 使用了一个 retry 循环来应对 CAS 失败的情况。循环内部先读取当前 ctl 值,检查运行状态是否允许创建新线程,然后尝试 CAS 将 ctl 加 1。如果 CAS 成功,就跳出循环继续创建 Worker 对象;如果失败,说明其他线程已经修改了 ctl,需要重新读取并检查状态。这种无锁的乐观并发控制比使用 synchronized 或 Lock 更加轻量,尤其在线程池频繁调整线程数量的场景下,性能优势非常明显。下面的代码片段展示了 addWorker 中与 ctl 相关的核心逻辑。
private boolean addWorker(Runnable firstTask, boolean core) {
retry:
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);
// Check if queue empty only if necessary.
if (rs >= SHUTDOWN &&
! (rs == SHUTDOWN &&
firstTask == null &&
! workQueue.isEmpty()))
return false;
for (;;) {
int wc = workerCountOf(c);
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maximumPoolSize))
return false;
if (compareAndIncrementWorkerCount(c))
break retry;
c = ctl.get(); // Re-read ctl
if (runStateOf(c) != rs)
continue retry;
// else CAS failed due to workerCount change; retry inner loop
}
}
// ... 后续创建 Worker 并启动线程
}
在 tryTerminate 方法中,ctl 的状态位也会通过 CAS 被修改。当线程池需要从 TIDYING 过渡到 TERMINATED 时,代码会执行 ctl.compareAndSet(c, ctlOf(TIDYING, 0)) 和 ctl.compareAndSet(c, ctlOf(TERMINATED, 0))。由于 ctlOf 会重新组合状态和线程数,这里可以借助 CAS 的原子性保证状态转换的唯一性,避免多个线程同时触发终止流程。整个过程中,状态和线程数始终被封装在同一个原子变量中,不会出现状态和数量不一致的中间状态。
设计权衡与常见疑问
有人可能会问,为什么状态占用 3 位而不是 2 位或 4 位?其实 2 位最多只能表示 4 种状态,而 Java 线程池有 5 种状态,所以至少需要 3 位。4 位虽然可以表示 16 种状态,但会挤占低位线程数的空间,使得可容纳的最大线程数从 2 的 29 次方减 1 缩小到 2 的 28 次方减 1。在实际使用中,5 种状态已经覆盖了线程池生命周期的所有阶段,3 位是既能满足需求又尽量保留线程数容量的最优选择。
为什么最大线程数只能是 536870911,而不是 2 的 31 次方减 1?因为最高 3 位被状态占用,int 类型的 32 位中只剩下 29 位可用于计数。如果强行把线程数增加到超过 CAPACITY,低位会向高位进位,从而破坏状态位的值,导致线程池状态被意外修改。例如当前状态为 SHUTDOWN(高 3 位 000),线程数为 CAPACITY 时 ctl 的低 29 位全为 1,再加 1 就会变成 00100000 00000000 00000000 00000000,高 3 位变成 001,也就是 STOP 状态,这会造成严重错误。因此所有增加线程数的操作都必须先检查 workerCountOf(c) 是否已经达到 CAPACITY。
为什么不分别使用两个 volatile 变量来保存状态和线程数?虽然 volatile 能保证可见性,但无法保证复合操作的原子性。如果状态和数量分散在两个变量中,那么先改状态再改数量,或者先改数量再改状态,中间都会出现窗口期,其他线程可能观察到不一致的组合。而 ctl 把两者压缩到一个 int 里,任何一次 CAS 或者普通的读操作都是针对整个组合值的,天然保证了状态与数量的强一致性。这也是为什么 ThreadPoolExecutor 的作者选择这种紧凑位域设计的重要原因。下面的代码演示了当线程数达到 CAPACITY 后继续递增会产生的问题。
int c = ctlOf(RUNNING, CAPACITY); // 低 29 位全 1 System.out.println(Integer.toBinaryString(c)); // 输出: 11100000000000000000000000000000 // 状态是 RUNNING,线程数达到上限 // 错误地对 c 加 1 int c2 = c + 1; System.out.println(Integer.toBinaryString(c2)); // 输出: 00100000000000000000000000000000 // 高 3 位变成 001,状态被破坏为 STOP
理解 ctl 的位运算设计之后,再去看线程池的 execute、shutdown、shutdownNow 等方法,会发现很多复杂的并发控制逻辑都建立在 ctl 这个看似简单的 int 之上。它用最小的内存开销和最少的同步操作,同时维护了线程池的生命周期状态和工作线程数量,体现了位运算在系统级编程中的精巧价值。