你知道ThreadPoolExecutor是怎么存储线程池状态和线程数量的么?

前言
最近在看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; }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
这段代码就是存储工作线程数和当前的线程池的状态的

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; }
1
2
那我们看下 是怎么计算的,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中也是要高低位 分别来存储读写锁的重入次数的
————————————————
版权声明:本文为CSDN博主「burgxun」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/zxlp520/article/details/107398462

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章