詳解Java執行緒池的ctl(執行緒池控制狀態)【原始碼分析】

moonfair發表於2020-08-12

0.綜述

  1.  ctl 是執行緒池原始碼中常常用到的一個變數。
  2. 它的主要作用是記錄執行緒池的生命週期狀態和當前工作的執行緒數。
  3. 作者通過巧妙的設計,將一個整型變數按二進位制位分成兩部分,分別表示兩個資訊。

1.宣告與初始化

  原始碼:

1 private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));

  分析一波:

  1.  ctl (執行緒池控制狀態)是原子整型的,這意味這對它進行的操作具有原子性。
  2. 如此一來,作為 ctl 組成部分的 runState (執行緒池生命週期狀態)和 workerCount (工作執行緒數) 也將同時具有原子性。
  3.  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;

  分析一波:

  1. COUNT_BITS  常量的值為 Integer.SIZE - 3 ,其中 Integer.SIZE 為整型最大位數,在本文剩餘部分,我們取其為 32 。
  2. 如此 COUNT_BITS 實際的值其實就是 29 。這裡有些讀者可能會有 “為什麼減去的數是 3 而不是別的” 的疑惑,這將在後文得到解答。
  3. CAPACITY  常量的值為  (1 << COUNT_BITS) - 1 ,其中 << 為左移運算子,這麼說可能不太直觀,我以二進位制直接寫出這個數將有助於理解:
    1
    0000 0000 0000 0001
    1 << 29 - 1
    0001 1111 1111 1111
  4. 因此在接下來的程式碼中, COUNT_BITS 就用來表示分隔runState 和workerCount 的位數;
  5. 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;

  分析一波:

  1. 這裡解答了上邊關於 COUNT_BITS 變數為什麼要減 3 的問題:因為執行緒池的生命週期有 5 個狀態,為了表達這 5 個狀態,我們需要 3 個二進位制位。
  2. 執行緒池的生命週期有興趣的讀者請百度 執行緒池生命週期 ;不明白為什麼 5 個狀態需要 3 個二進位制位的請百度 二進位制 。
  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; }

  分析一波:

  1. 此處我們解答了 CAPACITY 常量的作用,之前提到過,他是一個後 29 位均為 1 ,前 3 位為 0 的整數,因此我們可以通過:
  2. 對 CAPACITY 和 ctl 進行 & (按位與)操作就能取到 ctl 的後 29 位,即  workerCount 。
  3. 對 CAPACITY 進行 ~ (按位取反)操作後,再和 ctl 進行 & 操作就能取到 runState 。它的高 3 位是 ctl 的高 3 位,低 29 位為 0。這也解釋了為什麼之前提到的生命週期常量要在 -1 ~ 3 的基礎上再左移 29 位,因為不在常量初始化處左移的話就要在拆包的時候右移來保證取到的是正確的數值。然而拆包操作是要經常進行的,而常量的初始化只有一次。兩下對比,明顯在初始化時左移是效率更高的選擇。
  4. 除了拆包時的效率,常量初始化時左移也提高了打包函式的效率:此處打包函式可以直接對 runState 和 workerCount 進行 | (按位或)操作來得到 ctl 變數,就是因為 runState 的高 3 位為有效資訊,而 workerCount 的低 29 位為有效資訊,合起來正好得到一個含 32 位有效資訊的整型變數。
  5. 說到這裡可能仍有些讓人疑惑,我將再以二進位制的形式表示出所有涉及到的變數/常量:
    //下文中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     }

 

  分析一波:

  1. 注意這裡傳入的s是用了之前定義的生命週期常量。
  2. 這裡判斷狀態的大小時,直接將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     }

  分析一波:

  1. 注意到這裡的修改都使用了原子整型的CAS方法。

7.修改ctl中runState的大小

  原始碼:

1 ctl.compareAndSet(c, ctlOf(targetState, workerCountOf(c)))

  分析一波:

  1. 注意到修改 runState 並沒有再提供專門的方法,而是直接使用了原子整型的CAS方法來替換原來的 ctl 。

 8.仍存在的疑問

  • Q1:如果經過遞增 compareAndIncrementWorkerCount ,使得 workerCount 的大小超過29位,會發生什麼?會有安全檢查嗎?
  • Q2:為什麼為 workerCount 的修改提供了方法,卻沒有為 runState 的修改提供?

相關文章