ThreadPoolExecutor源码分析2-线程池状态

引言

声明:非原创内容,基于网上的分享自己再去阅读分析后的梳理总结,请看参考资源。

阅读源码的过程中,发现有关线程池状态的字段和方法使用了移位运算,很不直观,不容易理解,很影响我阅读核心代码,所以这一篇先把这个拦路虎给解决掉。

线程池的状态

上一篇我们已经知道 ThreadPoolExecutor 内部使用阻塞队列 BlockingQueue 来保存用户提交的任务。

状态枚举

ThreadPoolExecutor 线程池的状态被称为 runState,有 5 个状态:

  • RUNNING:可接受新的任务,也可处理阻塞队列里的任务
  • SHUTDOWN:不再接受新的任务,但可以处理阻塞队列里的任务
  • STOP:不再接受新的任务,也不会处理阻塞队列里的任务,且会中断正在处理的任务
  • TIDYING:过渡状态。所有的任务都终止了(可能是执行完了,也可能是被中断了),有效线程数 workerCount 也为 0 了,此时线程池的状态就转变为 TIDYING,并且将要调用钩子方法 terminated()。
  • TERMINATED:终止状态。terminated() 方法调用完成后的状态

这些值按照数值大小是有序的,这点很重要,可以进行次序比较。runState 随着时间单调递增,但不需要走过每一个状态。请看状态变迁图:

线程池状态变迁图

相关源码分析

线程池的控制状态(control state),简称为 ctl,是一个 atomic integer,它打包了两个概念性的字段:

  • workerCount, 表示线程的有效数量
  • runState, 指示状态是 running, shutdown 以及其他

ctl 的低 29 位表示 workerCount,高 3 位表示 runState。这样我们就将 workerCount 限制为最多 (2^29)-1 个线程(大约 5 亿个)。

workerCount 就是被允许启动(start),但未被允许停止(stop)的线程的数量。

现在来看看具体如何表示这两个概念的。整型中 32 位的前 3 位用来表示状态,后 29 位表示有效线程数,也被称为线程池容量。

1
2
3
// Integer.SIZE 就是二进制补码形式的 int 值使用的二进制位数,即 32
// 线程池容量所占用的位数
private static final int COUNT_BITS = Integer.SIZE - 3;

为了方便描述和理解,使用符号 {<0|1>:n 位} 表示 0 或 1 有多少位。

线程池容量大小为 1 << 29 - 1,1 << 29 结果是 001{0:29位},再减 1 就是 000{1:29位},这个值等于 (2^29)-1。有人可能会问,(2^29) 为啥要再减 1 呢?因为 (2^29) 是从 0 开始能表示多少个数,减去 1 才是能表示的最大的数。代码如下:

1
private static final int CAPACITY   = (1 << COUNT_BITS) - 1;

负数在计算机中使用二进制补码来表示。如何计算补码?先将十进制数表示成二进制,所有位取反,再加 1。

RUNNING 状态,-1 使用 1 的补码表示就是 {1:32位},-1 << 29 = {1:32位} << 29 = 111{0:29位},前 3 位为 111

1
private static final int RUNNING    = -1 << COUNT_BITS;

SHUTDOWN 状态,0 << 29 = {0:32位} << 29 = {0:32位},前 3 位为 000

1
private static final int SHUTDOWN   =  0 << COUNT_BITS;

STOP 状态,1 << 29 = {0:31位}1 << 29 = 001{0:29位},前 3 位为 001

1
private static final int STOP       =  1 << COUNT_BITS;

TIDYING 状态,2 << 29 = {0:30位}10 << 29 = 010{0:29位},前 3 位为 010

1
private static final int TIDYING    =  2 << COUNT_BITS;

TERMINATED 状态,3 << 29 = {0:30位}11 << 29 = 011{0:29位},前 3 位为 011

1
private static final int TERMINATED =  3 << COUNT_BITS;

这些状态值使用了高 3 位,低 29 位全部是 0。搞清楚状态位后,先来看看状态和有效线程数的初始化:

1
2
3
// 这个字段就是线程池的 control state
// 状态初始化为 RUNNING,有效线程数为 0
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));

接着看看获取、设置状态和有效线程数的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13

// 注意,对于下面这些方法,传给形参 c 的实参就是 ctl 字段的 int 值

// CAPACITY 为 000{1:29位},按位求反(~ 操作符)后的结果是 111{0:29位}
// 按位或的结果就是 {ctl的高三位}{0:29位}
private static int runStateOf(int c) { return c & ~CAPACITY; }

// 返回结果为 000{ctl的低29位}
private static int workerCountOf(int c) { return c & CAPACITY; }

// 将 runState 和 workerCount 打包为一个 int 值,用来设置 ctl 字段
// 取 rs 的高 3 位和 wc 的低 29 位,组成一个 32 位的整数值
private static int ctlOf(int rs, int wc) { return rs | wc; }

对状态进行比较的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

// 对于下面这些方法, 传给 c 的是 ctl 的 int value,s 是状态常量
// 将 ctl 表示的当前状态跟给定的状态常量进行比较

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;
}

为啥 Doug Lea 不使用两个单独的字段来表示 runState 和 workerCount,而是打包成一个 ctl 字段?

从代码来看,ctl 使用了基于 CAS 的原子操作,并且可以一次原子地修改两个属性值,既保证线程安全,也改进了并发性能。

参考资源

Java线程池ThreadPoolExecutor源码分析
JDK ThreadPoolExecutor 源码

0%