前言
最近在看ThreadPoolExecutor的源码,里面在处理存储线程池的状态和线程池里面的大小感觉特比有意思,所以单独拿出来和大家分享下~
怎么去存储状态和工作线程数,我们一步步的来看看,最后最下总结,总结下为什么这么去做
分析
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
private static final int COUNT_BITS = Integer.SIZE - 3;//32-3=29
private static final int CAPACITY = (1 << COUNT_BITS) - 1;//2的29次方-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 & ~CAPACITY; }
private static int workerCountOf(int c) { return c & CAPACITY; }
private static int ctlOf(int rs, int wc) { return rs | wc; }
这段代码就是存储工作线程数和当前的线程池的状态的
ThreadPoolExecutor 用ctl来存储当前的状态和当前的线程数的,这段代码 挺有意思的,大量的逻辑运算在里面 ,新手一上来看 本懵逼,其实一开始 我也是的。
先说下结论 这个是用了32位中的高三位去存储了当前的线程状态,后面的用来存储线程数量,所以线程数量,理论上最大是2的29次方-1.也就是上面的CAPACITY的10进制值。
运算符
现在我们就来 仔细的看下 是怎么做的?怎么存储?怎么使用的?看之前 可能要掌握下几个逻辑运算符
- & 与运算符:运算规则 0&0=0; 0&1=0; 1&0=0; 1&1=1;翻译成大白话就是 2个二进制位都是1的是 结果就是1,否则是0
- | 或运算符:运算规则 0|0=0; 0|1=1; 1|0=1; 1|1=1;翻译成大白话就是 2个二进制位只要有一个是1 那就结果就是1 否者为0;
- << 左位移运算符: 比如 3 <<2 3的8位二进制是 0000 0011 然后左移2位 结果就是 0000 1100
- ~ 非运算符: 规则就是所有的二进制取反 比如3的8位二进制0000 0011 ~3的8位二进制就是 1111 1100
好了 只有掌握了 上面的逻辑运算符 才能看懂怎么去做的,不然一脸懵逼,大学里面学的都还给老师了!
字段分析
-
COUNTBITS:我们首先来看下COUNTBITS 的值是Integer.SIZE -3,我们知道Integer的最大是32位,Integer.SIZE值也是32,那COUNTBITS的值就是29。
-
CAPACITY :接下来我们看下CAPACITY的值 (1 << COUNTBITS) - 1 , COUNTBITS的值 上面计算得到是29, (1 << 29) 这个怎么算呢,首先1的32位二进制是 0000 0000 0000 0000 0000 0000 0000 0001,那左移29位 结果是:0010 0000 0000 0000 0000 0000 0000 0000,那这个结果再减去1是:0001 1111 1111 1111 1111 1111 1111 1111;
-
RUNNING:RUNNING的值是:-1 << COUNT_BITS,其实也就是 -1<<29,
- 我们都知道Intger 最大长度是32位 ,最大容量是2的32次方-1,为什么是这样呢,因为虽然是32位,但是二进制的首位是存储的符号位,也是正数还是负数,打个比方 如果是8位二进制的话,最大的容量是0111 1111 ,结果是2的8次方-1等于127,
- 这边再次科普一个知识点 就是我们系统中都是以补码的形势去存储的,为什么这么存这个不清楚的 可以看下这篇文章: https://blog.csdn.net/zl10086111/article/details/80907428;
- -1 怎么存储的的看下 下面的表格计算
类别 | 32位二进制值 |
---|---|
原码 | 1000 0000 0000 0000 0000 0000 0000 0001 |
反码 | 0111 1111 1111 1111 1111 1111 1111 1110 |
补码 | 1111 1111 1111 1111 1111 1111 1111 1111 |
<< 29 | 1110 0000 0000 0000 0000 0000 0000 0000 |
那后面的 几个状态我就不一一列举了,
状态值 | 32位二进制值 |
---|---|
RUNNING | 1110 0000 0000 0000 0000 0000 0000 0000 |
SHUTDOWN | 0000 0000 0000 0000 0000 0000 0000 0000 |
STOP | 0010 0000 0000 0000 0000 0000 0000 0000 |
TIDYING | 0100 0000 0000 0000 0000 0000 0000 0000 |
TERMINATED | 0110 0000 0000 0000 0000 0000 0000 0000 |
好了 看完了上面的状态ctl 是什么
ctl
首先我们从代码找那个看 ctl 是一个AtomicInteger 类型,是一个原子类,关于原子类我相信大家应该知道,不清楚的可是要去看看了。
那我们看下ctl 的组成,英文注解也说的很清楚了,是由workerCount要就是说线程词中运行的数量和runState线程词的状态组成的。
我们看下调用的方法ctlOf, rs | wc 这是将线程池状态和线程数量大小 做或运算,或运算我上面也讲过,2个二进制位位中 只有一个是1 那结果就是1,那也就是说不管线程池中哪个runState状态和多少线程数量的大小相或,高三位必将得到保留,低29位就是 线程数量的大小值,举例说明下
变量名称 | 32位二进制值 |
---|---|
rs:RUNNING | 1110 0000 0000 0000 0000 0000 0000 0000 |
wc:5 | 0000 0000 0000 0000 0000 0000 0000 0101 |
ctl: rs 或 wc | 1110 0000 0000 0000 0000 0000 0000 0101 |
看下结果 是不是高三位都被保留下来,低位值就是线程数量的大小值
runStateOf/workerCountOf
runStateOf 方法是我们用来获取当前的线程池状态,我们知道线程池的状态是存在ctl的高三位里面的,那我们是怎么去获取这个高三位的呢
private static int runStateOf(int c) { return c & ~CAPACITY; }
private static int workerCountOf(int c) { return c & CAPACITY; }
那我们看下 是怎么计算的,c就是我们获取的ctl的值,我们就用上面计算好的
变量名称 | 32位二进制值 |
---|---|
c: | 1110 0000 0000 0000 0000 0000 0000 0101 |
CAPACITY | 0001 1111 1111 1111 1111 1111 1111 1111 |
~CAPACITY | 1110 0000 0000 0000 0000 0000 0000 0000 |
c & ~CAPACITY | 1110 0000 0000 0000 0000 0000 0000 0000 |
看下得到的结果 是不是就是我们的RUNNING状态的值,其实~CAPACITY的高三位全部是1,低29位全部是0,这个值不管和那个值做逻辑与运算,得到的值都是保留了高三位的结果的。
变量名称 | 32位二进制值 |
---|---|
w: | 0000 0000 0000 0000 0000 0000 0000 0101 |
CAPACITY | 0001 1111 1111 1111 1111 1111 1111 1111 |
w & CAPACITY | 0000 0000 0000 0000 0000 0000 0000 0101 |
再看下这个结果 是不是就是我们刚才的5,这2个运算利用的非常巧妙,这个运算刚好保留了低位的值。和上面的刚好相反。
总结
好了,经过上面的一步步的分析,我相信聪明的你 一定能明白怎么回事了,写Juc的作者真的很厉害,细节都运用的很巧妙,一开始看的时候确实很懵逼,等看明白后感觉确实很巧妙,大家看的时候千万要看java文件的代码,不要看class文件的代码,如果你看了会更懵逼,哪个上面都是数字!
最后来说下 为什么原作者要这么去设计,我们知道ThreadPoolExecutor这个是在JUC包中的,JUC是处理java 并发多线程的一个包,那作者这么设计 当然不是为减少字段的存储而将2个字段合并,而是考虑到在并发多线程的情况下,要保证这2个字段的同步,虽然我们可以用CAS去更新字段,但是2个字段都CAS也不能完全保证执行同一,如果这个时候合并成一个字段的话,那我用CAS的操作一定没问题了,比如新增线程数大小的时候就用AtomicInteger的compareAndSet去新增
其实在JUC中也存在很多这样的处理,比如我记得我之前也看过ReentrantReadWriteLock中也是要高低位 分别来存储读写锁的重入次数的