你知道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

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