详解Java线程池的ctl(线程池控制状态)【源码分析】
0.综述
- ctl 是线程池源码中常常用到的一个变量。
- 它的主要作用是记录线程池的生命周期状态和当前工作的线程数。
- 作者通过巧妙的设计,将一个整型变量按二进制位分成两部分,分别表示两个信息。
1.声明与初始化
源码:
1 private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
分析一波:
- ctl (线程池控制状态)是原子整型的,这意味这对它进行的操作具有原子性。
- 如此一来,作为 ctl 组成部分的 runState (线程池生命周期状态)和 workerCount (工作线程数) 也将同时具有原子性。
- ThreadPoolExecutor 使用 ctlOf 方法来将 runState 和 workerCount 两个变量(都是整型)打包成一个 ctl 变量。稍后将解读这个方法的实现。
2.两个工具人常量 COUNT_BITS 和 CAPACITY
源码:
1 private static final int COUNT_BITS = Integer.SIZE - 3; 2 private static final int CAPACITY = (1 << COUNT_BITS) - 1;
分析一波:
- COUNT_BITS 常量的值为 Integer.SIZE - 3 ,其中 Integer.SIZE 为整型最大位数,在本文剩余部分,我们取其为 32 。
- 如此 COUNT_BITS 实际的值其实就是 29 。这里有些读者可能会有 “为什么减去的数是 3 而不是别的” 的疑惑,这将在后文得到解答。
- CAPACITY 常量的值为 (1 << COUNT_BITS) - 1 ,其中 << 为左移运算符,这么说可能不太直观,我以二进制直接写出这个数将有助于理解:
1 0000 0000 0000 0001 1 << 29 - 1 0001 1111 1111 1111
- 因此在接下来的代码中, COUNT_BITS 就用来表示分隔runState 和workerCount 的位数;
- 而CAPACITY 则作为取这两个变量( runState 和 workerCount )的工具(具体是怎么使用的请看下文)
3.线程池生命周期状态常量
源码:
1 private static final int RUNNING = -1 << COUNT_BITS; 2 private static final int SHUTDOWN = 0 << COUNT_BITS; 3 private static final int STOP = 1 << COUNT_BITS; 4 private static final int TIDYING = 2 << COUNT_BITS; 5 private static final int TERMINATED = 3 << COUNT_BITS;
分析一波:
- 这里解答了上边关于 COUNT_BITS 变量为什么要减 3 的问题:因为线程池的生命周期有 5 个状态,为了表达这 5 个状态,我们需要 3 个二进制位。
- 对线程池的生命周期有兴趣的读者请百度 线程池生命周期 ;不明白为什么 5 个状态需要 3 个二进制位的请百度 二进制 。
- 注意到这里标注状态使用的并不是 -1 ~ 3 ,而是这 5 个数字分别左移 COUNT_BITS 位,这样做的好处将在接下来的代码中得到体现。
4.打包函数与拆包函数
源码:
1 //拆包函数 2 private static int runStateOf(int c) { return c & ~CAPACITY; } 3 private static int workerCountOf(int c) { return c & CAPACITY; } 4 //打包函数 5 private static int ctlOf(int rs, int wc) { return rs | wc; }
分析一波:
- 此处我们解答了 CAPACITY 常量的作用,之前提到过,他是一个后 29 位均为 1 ,前 3 位为 0 的整数,因此我们可以通过:
- 对 CAPACITY 和 ctl 进行 & (按位与)操作就能取到 ctl 的后 29 位,即 workerCount 。
- 对 CAPACITY 进行 ~ (按位取反)操作后,再和 ctl 进行 & 操作就能取到 runState 。它的高 3 位是 ctl 的高 3 位,低 29 位为 0。这也解释了为什么之前提到的生命周期常量要在 -1 ~ 3 的基础上再左移 29 位,因为不在常量初始化处左移的话就要在拆包的时候右移来保证取到的是正确的数值。然而拆包操作是要经常进行的,而常量的初始化只有一次。两下对比,明显在初始化时左移是效率更高的选择。
- 除了拆包时的效率,常量初始化时左移也提高了打包函数的效率:此处打包函数可以直接对 runState 和 workerCount 进行 | (按位或)操作来得到 ctl 变量,就是因为 runState 的高 3 位为有效信息,而 workerCount 的低 29 位为有效信息,合起来正好得到一个含 32 位有效信息的整型变量。
- 说到这里可能仍有些让人疑惑,我将再以二进制的形式表示出所有涉及到的变量/常量:
//下文中a和b分别代表runState和workerCount的有效信息 //CAPACITY 0001 1111 1111 1111 //ctl aaab bbbb bbbb bbbb //runState aaa0 0000 0000 0000 //workerCount 000b bbbb bbbb bbbb
5.运行状态的判断
源码:
1 private static boolean runStateLessThan(int c, int s) { 2 return c < s; 3 } 4 5 private static boolean runStateAtLeast(int c, int s) { 6 return c >= s; 7 } 8 9 private static boolean isRunning(int c) { 10 return c < SHUTDOWN; 11 }
分析一波:
- 注意这里传入的s是用了之前定义的生命周期常量。
- 这里判断状态的大小时,直接将c 和s 进行了比较,这是因为代表状态的信息占据了两个变量的高 3 位,而比较高位的大小时,低位是没有影响的。
6.修改ctl中workCount的大小
源码:
1 private boolean compareAndIncrementWorkerCount(int expect) { 2 return ctl.compareAndSet(expect, expect + 1); 3 } 4 5 private boolean compareAndDecrementWorkerCount(int expect) { 6 return ctl.compareAndSet(expect, expect - 1); 7 } 8 9 private void decrementWorkerCount() { 10 do {} while (! compareAndDecrementWorkerCount(ctl.get())); 11 }
分析一波:
- 注意到这里的修改都使用了原子整型的CAS方法。
7.修改ctl中runState的大小
源码:
1 ctl.compareAndSet(c, ctlOf(targetState, workerCountOf(c)))
分析一波:
- 注意到修改 runState 并没有再提供专门的方法,而是直接使用了原子整型的CAS方法来替换原来的 ctl 。
8.仍存在的疑问
- Q1:如果经过递增 compareAndIncrementWorkerCount ,使得 workerCount 的大小超过29位,会发生什么?会有安全检查吗?
- A1:有安全检查,在ThreadPoolExecutor类的addWorker方法中有这样一行代码:
1 if (wc >= CAPACITY || 2 wc >= (core ? corePoolSize : maximumPoolSize)) 3 return false;
- Q2:为什么为 workerCount 的修改提供了方法,却没有为 runState 的修改提供?