承上啟下:上一篇文章小豹子講了我為什麼想要研究執行緒池的程式碼,以及我計劃要怎樣閱讀程式碼。這篇文章我主要閱讀了執行緒池例項化相關的程式碼,並提出了自己的疑問。
3 千里之行,始於例項化
3.1 先建立一個執行緒池玩玩
我們首先看構造器的宣告,ThreadPoolExecutor
有四個過載構造器,其中三個分別指定了不同的預設引數值,我們直接看引數最全的構造器:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
複製程式碼
引數有點多,我們有點懵,但並不是無從下手。我們去看程式碼上方的 JavaDoc:
- corePoolSize:要保留在池中的執行緒數。即便執行緒空閒,不小於該引數的執行緒也將被保留。除非設定了
allowCoreThreadTimeOut
- maximumPoolSize:池中允許的最大執行緒數
- keepAliveTime:當池中執行緒數大於核心池數量(
corePoolSize
)時,大於核心池數量部分的執行緒空閒持續keepAliveTime
時間後,將被終止 - unit:
keepAliveTime
引數的時間單位 - workQueue:在任務被執行之前用於儲存任務的佇列。這個佇列只包含由
execute
方法提交的Runnable
任務 - threadFactory:
executor
建立新執行緒時使用的執行緒工廠 - handler:用於處理由於超過執行緒上限或佇列上限而產生的拒絕服務異常
那麼我們根據文件來建立一個執行緒池:
@Test
public void newInstanceTest() {
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 10, 60, TimeUnit.SECONDS,
new LinkedBlockingQueue<Runnable>(),
new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
return new Thread();
}
}, new RejectedExecutionHandler() {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
System.out.println("拒絕服務");
}
});
}
複製程式碼
這裡我們建立了一個核心池數量為 5,最大執行緒數為 10,執行緒保持時間為 60 秒的執行緒池。
3.2 初始化時,執行緒池做了什麼?
我們跟蹤到程式碼中,看例項化的過程中,構造器為我們做了什麼:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.acc = System.getSecurityManager() == null ?
null :
AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
複製程式碼
這裡很容易理解,前面進行了輸入引數的檢查,this.acc
是訪問控制器上下文,這裡我們不深入研究它。唯一值得一提的就是 unit.toNanos(keepAliveTime)
,這是將引數中的 keepAliveTime
轉換成納秒,似乎也不難理解,但我有一個疑問:為什麼要抽象時間單位?抽象時間段不好麼?比如我設計一個 Period
類表示一段時間,裡面有幾個靜態方法用於例項化,比如 Period.fromSeconds(long n)
表示 n
秒的一段時間,然後可以使用 Period#toNanos()
這類的方法將該段時間傳化為納秒。這樣可以是引數更簡潔,表意更明確。不知兩種設計方案的優缺,還望各位指點。
我們繼續看 ThreadPoolExecutor
的初始化:
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
private static final int COUNT_BITS = Integer.SIZE - 3;
private static final int CAPACITY = (1 << COUNT_BITS) - 1;
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;
複製程式碼
又是一堆天書,但似乎 RUNNING
、SHUTDOWN
等是表示某種狀態的常量,至於它們的賦值為什麼這麼特殊,其他變(常)量都是幹嘛的?老套路,看文件。
文件告訴我們:ctl
是表示執行緒池狀態的原子整形,它包含兩部分:工作執行緒數、執行狀態。為了將兩個變數用一個原子整形表示,我們限制工作執行緒數最多隻能有 (2^29)-1
(大概 5 億)個,而空餘的高三位用來儲存執行狀態。
執行狀態可能有這些值:
- RUNNING:允許提交新任務,處理佇列中的任務
- SHUTDOWN:不允許提交新任務,但處理佇列中的任務
- STOP:不允許提交新任務,不處理佇列中的任務,打斷執行中的任務
- TIDYING:所有任務已經終止,工作執行緒數為零,執行緒過渡到
TIDYING
時將呼叫terminated()
回撥方法 - TERMINATED:
terminated()
方法完成後
這些值之間的順序很重要,執行狀態的值隨時間單調遞增,但在一個生命週期內不需要經歷過所有的狀態。
狀態的轉換:
- RUNNING -> SHUTDOWN:呼叫
shutdown()
觸發,或者隱含在finalize()
中 - (RUNNING / SHUTDOWN) -> STOP:呼叫
shutdownNow()
觸發 - SHUTDOWN -> TIDYING:當佇列和池均為空時觸發
- STOP -> TIDYING:當池為空時觸發
- TIDYING -> TERMINATED:
terminated()
執行結束之後
看過文件之後,我們再回頭看這幾個常量的賦值:首先 COUNT_BITS
是 Integer
的長度減 3,其他幾個狀態量分別是 -1、0、1,2,3 向高位移動 COUNT_BITS
位的結果,這也就對應著文件所寫,用一個整形的高三位來儲存執行緒池的狀態。CAPACITY
的值是 1 向高位移動 COUNT_BITS
位再減一,字面意思是容量,這不難理解,COUNT_BITS
就是代表執行緒池所能容納的最大執行緒數,而值得一提的是,這個值在二進位制層面上具有另一個意義:CAPACITY
的二進位制值高三位為 0,其他位為 1。具體用途,我們後面細說。
現在只剩 ctl
我們不清楚了,首先從文件中我們可以獲知 ctl
是包含了執行狀態與執行緒數量的一個整形原子變數,那麼 ctlOf(RUNNING, 0)
是什麼意思呢?我們來看 ThreadPoolExecutor
中的靜態方法:
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; }
private static boolean runStateLessThan(int c, int s) {
return c < s;
}
private static boolean runStateAtLeast(int c, int s) {
return c >= s;
}
private static boolean isRunning(int c) {
return c < SHUTDOWN;
}
複製程式碼
這裡小豹子帶大家回憶一下位運算:
&
是按位與運算子,輸入均為 1 輸出為 1,其他為 0;
|
是按位或運算子,輸入均為 0 輸出為 0,其他為 1;
~
是按位非運算子,輸入為 0 輸出為 1,輸入為 1 輸出為 0;
我們看 ctlOf(int rs, int wc)
,其中 rs 指執行狀態(runState),wc 值執行緒數(workerCount)。rs 值的特點是高三位表示執行狀態,而其他低位均為 0,wc 值的特點是高三位為 0(因為不大於 CAPACITY
嘛),低位表示執行緒數。那麼對兩個值進行按位或運算,正好就將兩個值的有效位合併到一個整形變數中。我們再回頭看 ctl
變數的初始化 new AtomicInteger(ctlOf(RUNNING, 0))
。這回應該就清楚了,ctlOf(RUNNING, 0)
表示執行狀態是 RUNNING
,執行緒數為 0 的執行緒池狀態。
那麼 runStateOf
與 workerCountOf
就不必多說,是從 ctl
中剝離出執行狀態值和執行緒數,在這裡 CAPACITY
的作用就體現出來,它表示一種標誌位,因為它二進位制值的特性(前文提到)使得另一個值與它進行位與(或非與)運算時可以得到值的低位(或高位)。接下來我著重解釋一下 isRunning(int c)
,首先我們要已知兩個事實:
- 執行狀態值之間的順序很重要,執行狀態的值隨時間單調遞增,
RUNNING
是最小的,SHUTDOWN
次之。 - 執行狀態儲存在
ctl
變數的高三位。
那麼判斷當前執行緒池的狀態是否為 RUNNING
,有沒有必要將 ctl
中的狀態值提取出來,再與 RUNNING
常量進行對比呢?沒有必要,因為狀態值佔高位,只要狀態值小於 SHUTDOWN
,ctl
就必然小於 SHUTDOWN
,而小於 SHUTDOWN
的狀態只有 RUNNING
,因此只要 ctl
值小於 SHUTDOWN
,它就一定是 RUNNING
狀態。其他函式(runStateLessThan
、runStateAtLeast
)同理,直接對比就好。
3.3 疑問
看到 ThreadPoolExecutor
中用一個原子變數儲存兩種狀態的設計思想,我心中產生一個疑問:為什麼要這樣做?為了節省記憶體麼?肯定不是,執行緒池的主要應用場景應該是伺服器,而用時間換空間(還只換了這麼點空間)是非常不值得的。那麼我唯一能想到的解釋是,有利於提高併發效能。
我記得我在看《高效能 MySQL》的時候,作者告訴我這樣一種思想:熱點分離。
書中描繪了這樣一個應用場景,一個類似微博的應用,後臺要統計總髮貼數。那麼每一次獲取資料都要 count(*)
這肯定不現實。現實一點的做法是,在資料庫中維護一個表示總髮貼數的記錄,每一次使用者發帖,這個值就加 1。這種方案併發效能也不是很好。因為這個欄位至少要加行鎖,每次使用者發帖,總髮貼數加 1 時都會引起鎖競爭。這相當於把使用者發帖行為序列化了。
書中的解決方案是設計一張表,其中有 n 條記錄(比如說 100 條),每一次使用者發帖,在這 100 條記錄中選一條記錄(可以是隨機選擇,也可以根據時間取模)自加 1。然後每隔一段時間將表中的所有記錄加和賦值到第一條記錄中,刪除其他記錄。這樣一來,原先是 N 個執行緒爭搶一把鎖,現在是 N 個執行緒爭搶一百把鎖。併發效能當然得到了增加。這就是所謂的熱點分離。
但 ThreadPoolExecutor
中 ctl
的設計似乎反其道而行之。把兩個需要併發訪問的值“捏”到了一起。除非執行狀態和執行緒數往往同時變化,否則這樣做,我理解不了它是怎樣提高併發效能的。我決定暫時擱置這個問題,在後續對原始碼的學習過程中,我相信我能得到答案。
系列文章
小豹子還是一個大三的學生,小豹子希望你能“批判性的”閱讀本文,對本文內容中不正確、不妥當之處進行嚴厲的批評,小豹子感激不盡。