書接上文,一文加深你對Java執行緒池的瞭解與使用—築基篇,本文將從執行緒池內部的最最核心類 ThreadPoolExecutor 原始碼中的重要方法入手,也是本文分析的物件,從狀態/任務/執行緒這三個模組剖析執行緒池的機制,掌握背後的核心設計。
一、執行緒池如何管理自身的狀態/生命週期
在ThreadPoolExecutor 類中,有以下的定義:
//Integer的範圍為[-2^31,2^31 -1], Integer.SIZE-3 =32-3= 29,用來輔助左移位運算
private static final int COUNT_BITS = Integer.SIZE - 3;
//(1 << 29) - 1=000011111111111111111111111111111,前三位是0,後29為1。
//常量值,被用以輔助與運算求出執行緒池執行狀態or執行緒池執行緒數量,見runStateOf與workerCountOf方法
private static final int CAPACITY = (1 << COUNT_BITS) - 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;
// 使用32位的變數ctl同時容納兩個重要概念的值,前3位儲存執行緒池自身狀態,後29位儲存執行緒數量
//runStateOf負責取出狀態值,workerCountOf負責取出執行緒數量值,ctlOf負責將兩個值合成到一個32位的值
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; }
//自增型Integer,初始化時會將 RUNNING | 0 合成到一起
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
從以上原始碼可以看出:執行緒池的自身狀態,共有5種,通過常量值方式定義下來,執行緒池被啟動後,執行緒池狀態儲存在32位的自增型Integer變數 ctl
的高位(前3位),類內其他方法是通過runStateOf(ctl)
方法位運算取出狀態常量值(前3位)。
那 ctl
剩餘29位的用途是什麼呢?——儲存執行緒池池內的活躍工作執行緒數量。執行緒池被啟動後,任務未被申請,執行緒當前數量為0,workerCountOf(ctl)
通過位運算取出後29位代表的工作執行緒數量值。
通過ctlOf(RUNNING, 0)
,將執行緒池狀態RUNNING與目前活躍執行緒數量0合成出一個32位的值賦值給ctl這個會自增的Integer
,用以儲存這兩個重要概念的值。
通過一個變數,巧妙地包含兩部分的資訊:執行緒池的執行狀態 (runState) 和執行緒池內有效執行緒的數量 (workerCount),兩者互不干擾,可避免在操作前做相關判斷時為了維護兩者的一致而佔用鎖和資源。
執行緒池狀態含義
執行緒池狀態 | 狀態含義 |
---|---|
RUNNING | 執行緒池被建立後的初始狀態,能接受新提交的任務,並且也能處理阻塞佇列中的任務。 |
SHUTDOWN | 關閉狀態,不再接受新提交的任務,但仍可以繼續處理已進入阻塞佇列中的任務。 |
STOP | 會中斷正在處理任務的執行緒,不能再接受新任務,也不繼續處理佇列中的任務, |
TIDYING | 所有的任務都已終止,workerCount(有效工作執行緒數)為0。 |
TERMINATED | 執行緒池執行徹底終止 |
執行緒池如何切換狀態
在官方給出的說明中,可以清晰看出執行緒池各個狀態轉變的觸發條件:
RUNNING -> SHUTDOWN:On invocation of shutdown(), perhaps implicitly in finalize()
(RUNNING or SHUTDOWN) -> STOP: On invocation of shutdownNow()
SHUTDOWN -> TIDYING: When both queue and pool are empty
STOP -> TIDYING: When pool is empty
TIDYING -> TERMINATED: When the terminated() hook method has completed
執行緒池狀態的生命週期
二、執行緒池如何管理任務
任務的排程機制
不論是哪一種類的執行緒池,呼叫execute往執行緒新增任務後,最後都會進入ThreadPoolExecutor.execute(runnable)
中,下面看一下這個方法有什麼名堂:
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
//獲取工作執行緒數量 與核心執行緒數對比
if (workerCountOf(c) < corePoolSize) {
//傳入true表示建立新執行緒時與核心執行緒數做比較,執行command
if (addWorker(command, true))
return;
c = ctl.get();
}
//來到此處,說明 工作執行緒數 已經大於 核心執行緒數
//短路原則,先判斷執行緒池狀態是否Running,處於Running則再判斷阻塞佇列是否可以儲存新任務
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
//短路原則,如果重檢查執行緒池狀態不在Running了,則嘗試remove(command)阻塞佇列移除此任務
if (! isRunning(recheck) && remove(command))
reject(command); //若佇列成功移除任務後,就拒絕掉此任務
else if (workerCountOf(recheck) == 0)//執行狀態,儲存在阻塞佇列,但工作執行緒目前為0
//null表示單純建立工作執行緒,傳入false表示建立新執行緒時與最大執行緒數做比較
addWorker(null, false);
}
//阻塞佇列不可以儲存任務,嘗試增加工作執行緒執行command
else if (!addWorker(command, false))
reject(command); //若增加執行緒操作也失敗了-->拒絕掉任務
}
在這短短的二十行程式碼裡,出現了多個 if else,說明任務排程還是有點複雜的,下面來逐步理清它。
此處的corePoolSize
是在建構函式中被賦值this.corePoolSize = corePoolSize;
,是一個固定下來的值。
execute方法是外界往執行緒池新增任務的入口,也是執行緒池內部首先接觸到外界任務的地方,它需要對任務的去向進行管理,對任務的管理有以下三個選項:
- 緩衝到佇列中等待執行——
workQueue.offer(command)
- 建立新的執行緒直接執行——
addWorker(command, false)
- 拒絕掉該任務,執行執行緒池當前的拒絕策略——
reject(command)
從
addWorker(Runnable firstTask, boolean core)
方法內部邏輯得知,如果能建立新執行緒成功說明此時執行緒池的狀態是Running,或者是SHUTDOWN下任務佇列非空但是不可有新任務,然後當前執行緒數量需小於比較物件(傳入true,則與核心執行緒數做比較,傳入false則與最大執行緒數作比較)
根據原始碼,可以總結下以下的判斷條件,需要綜合阻塞佇列狀態,當前工作執行緒數量,核心執行緒數,最大執行緒數這些執行緒池核心屬性進行判斷。
執行緒池狀態 | 當前工作執行緒數 | 阻塞佇列是否已滿 | 佇列是否已加入任務 | 任務的排程 |
---|---|---|---|---|
Running | 少於核心執行緒數 | / | 否 | 建立新的工作執行緒,直接執行任務 |
Running | 大於核心執行緒數 | 否 | 否 | 將任務加入到阻塞佇列,等待執行 |
非Running | / | / | 是 | 佇列移除任務,移除成功後拒絕該任務執行拒絕策略 |
Running | 0 | / | 是 | 建立新工作執行緒,但不執行任務 |
RUNNING | 大於核心執行緒數且小於最大執行緒數 | 是 | 否 | 建立新的工作執行緒,直接執行任務 |
RUNNING | 大於等於最大執行緒數 | 是 | 否 | 拒絕該任務,執行拒絕策略 |
非Running | 大於等於最大執行緒數 | / | 否 | 拒絕該任務執行拒絕策略 |
任務進隊
在execute()
中已經操作任務進隊,需要同時滿足執行緒池執行狀態為Running,當前工作執行緒數大於核心執行緒數,阻塞佇列非已滿這些條件。
從建構函式可以看出不同種類的阻塞佇列都實現了 BlockingQueue<Runnable>
介面 。
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
//···
}
因為BlockingQueue介面提供的操作可以自定義,所以也有了能適應各種不同場景的阻塞佇列。不同的阻塞佇列對 offer 方法的重寫也是各不相同。
下面以 CachedThreadPool
的SynchronousQueue
重寫的offer
方法為例:
public boolean offer(E e) {
if (e == null) throw new NullPointerException();
//transfer()傳入的e若為null則作為消費者,若非null則作為生產者
//運用transferer.transfer達到put前需要take,不儲存任務的目的
return transferer.transfer(e, true, 0) != null;
}
任務出隊
任務的出隊是任務管理模組與執行緒管理模組的聯絡,簡單來說任務從阻塞佇列中被取出說明有工作執行緒需要執行任務了。
任務被執行會有兩種可能:第一種是任務直接由新建立的執行緒執行。另一種是執行緒從任務佇列中獲取任務然後執行,執行完任務的空閒執行緒會再次去從佇列中取出任務再執行。
那麼任務是什麼時候又是怎樣地會被取出呢?從 addWorker
方法入手
private boolean addWorker(Runnable firstTask, boolean core) {
//... 省略
boolean workerStarted = false;
boolean workerAdded = false;
Worker w = null;
try {
w = new Worker(firstTask); //建立工作執行緒
final Thread t = w.thread; //獲取工作執行緒持有的執行緒例項
if (t != null) {
//省略獲取鎖,加入阻塞佇列,再次判斷執行緒池執行狀態部分的程式碼
if (workerAdded) {
t.start();
workerStarted = true;
}
}
...
return workerStarted;
}
接下來看下,這個持有執行緒例項的Worker
是什麼名堂:已省略非討論部分的程式碼
private final class Worker extends AbstractQueuedSynchronizer implements Runnable{
/** 具體執行任務的執行緒例項, 若執行緒工廠提供不了可能為null */
final Thread thread;
/** 初始執行的任務. 可能為 null */
Runnable firstTask;
/** 執行緒已完成任務計數器r */
volatile long completedTasks;
/**
* 初始化給定的firstTask和從執行緒工廠獲取執行緒例項
* @param firstTask the first task (null if none)
*/
Worker(Runnable firstTask) {
setState(-1); // inhibit interrupts until runWorker
this.firstTask = firstTask;
this.thread = getThreadFactory().newThread(this);
}
/** Delegates main run loop to outer runWorker */
public void run() {
runWorker(this);
}
//省略以下關於鎖和取消的方法
}
再來看下runWorker(this);
有什麼名堂:
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;
w.firstTask = null;
w.unlock(); // allow interrupts
boolean completedAbruptly = true;
try {
while (task != null || (task = getTask()) != null) {
//省略大量關於任務執行部分的程式碼
}
}
}
可以看出, runWorker(worker)
方法,主要做的是,先執行worker自帶的firstTask任務,再不斷地執行getTask(),要從阻塞佇列中獲取任務來執行。也就是說:任務被執行的第一種可能就是指執行緒被建立時帶有firstTask任務,會先執行掉firstTask。
下面再來看下這個從阻塞佇列中返回任務的getTask()
方法吧:
private Runnable getTask() {
boolean timedOut = false; // Did the last poll() time out?
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);
// 檢查佇列是否為空,檢查當前執行緒池的狀態
if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
decrementWorkerCount();
return null;
}
int wc = workerCountOf(c);
// Are workers subject to culling?
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
//阻塞,迴圈重複地判斷,直到不滿足條件,才執行下方的返回操作
if ((wc > maximumPoolSize || (timed && timedOut))
&& (wc > 1 || workQueue.isEmpty())) {
if (compareAndDecrementWorkerCount(c))
return null;
continue;
}
try {
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();
if (r != null)
return r; //返回任務
timedOut = true;
} catch (InterruptedException retry) {
timedOut = false;
}
}
由程式碼可以看到,裡面的邏輯主要在死迴圈 for( ; ? ,任務返回null的條件有兩個:第一個是執行緒池自身狀態為SHUTDOWN
且阻塞佇列為空佇列 ,第二個是執行緒池自身狀態生命週期處於STOP
,TIDYING
,TERMINATED
。在迴圈內會阻塞地一直通過ctl
進行判斷,直到滿足阻塞佇列不為空,有可用的工作執行緒,才會從阻塞佇列中取出任務返回。
任務的拒絕策略
根據執行緒池當前設定的拒絕策略RejectedExecutionHandler
來處理該任務, 若沒有指定那執行緒池的預設處理方式則是直接拋異常。
可選的拒絕策略
JDK提供的四種已有拒絕策略,其特點如下:
自定義拒絕策略
拒絕策略是一個介面,其設計如下:
public interface RejectedExecutionHandler {
void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
}
使用者可以通過實現這個介面去定製拒絕策略,在手動配置執行緒池時的建構函式傳入或者通過方法setRejectedExecutionHandler
線上程池執行期間改變拒絕任務的策略。
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
//省略其他程式碼
this.handler = handler;
}
public void setRejectedExecutionHandler(RejectedExecutionHandler handler) {
if (handler == null)
throw new NullPointerException();
this.handler = handler;
}
拒絕策略的執行
從呼叫execute()
方法,經過一系列判斷,當該任務被判斷需要被被拒絕後,會接著執行reject(command)
,最終就會執行具體實現RejectedExecutionHandler
介面的rejectedExecution(r,executor)
方法了。
final void reject(Runnable command) {
handler.rejectedExecution(command, this);
}
三、執行緒池如何管理工作執行緒
工作執行緒的增加
經歷哪些判斷,才能/才需要增加工作執行緒,通過什麼方式增加
增加執行緒是通過執行緒池中的addWorker(firstTask , core)
方法,這個方法也是執行緒管理模組中的核心方法。該方法最主要的功能是增加一個工作執行緒,執行它,返回操作是否成功這個結果。
addWorker(firstTask , core)
的引數:firstTask、core :
firstTask
引數用於指定新建立的執行緒要執行的第一個任務,若單純只建立一個工作執行緒則該引數為null這不會造成空指標問題;core
引數決定新增執行緒時當前活動執行緒數的比較物件,true比較物件為corePoolSize核心執行緒數,false比較物件是maximumPoolSize最大執行緒數,比較時要小於才能繼續建立工作執行緒。
操作結果返回false的原因
情況一:不滿足線上程池執行狀態處於SHUTDOWN
時,firstTask為null且阻塞佇列非空
情況二:執行緒池執行狀態處於STOP
,TIDYING
,TERMINATED
情況三:當前活動執行緒數大於corePoolSize / 當前活動執行緒數大於maximumPoolSize
在將新執行緒 w
加入阻塞佇列的過程要上鎖,防止對阻塞佇列的寫入有衝突
private boolean addWorker(Runnable firstTask, boolean core) {
retry:
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);
// 檢查執行緒池狀態和 阻塞佇列
if (rs >= SHUTDOWN &&
! (rs == SHUTDOWN &&
firstTask == null &&
! workQueue.isEmpty()))
return false;
for (;;) {
int wc = workerCountOf(c);
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maximumPoolSize))
return false;
if (compareAndIncrementWorkerCount(c))
break retry;
c = ctl.get(); // Re-read ctl
if (runStateOf(c) != rs)
continue retry;
// else CAS failed due to workerCount change; retry inner loop
}
}
boolean workerStarted = false;
boolean workerAdded = false;
Worker w = null;
try {
w = new Worker(firstTask); //建立工作執行緒例項
final Thread t = w.thread; //獲取工作執行緒持有的執行緒例項
if (t != null) {
final ReentrantLock mainLock = this.mainLock; //獲取可重入鎖
mainLock.lock();
try {
// Recheck while holding lock.
// Back out on ThreadFactory failure or if
// shut down before lock acquired.
int rs = runStateOf(ctl.get());
if (rs < SHUTDOWN ||
(rs == SHUTDOWN && firstTask == null)) {
if (t.isAlive()) // 預檢查執行緒是否能啟動
throw new IllegalThreadStateException();
workers.add(w); //往阻塞佇列中新增任務
int s = workers.size();
if (s > largestPoolSize)
largestPoolSize = s;
workerAdded = true;
}
} finally {
mainLock.unlock();
}
if (workerAdded) {
t.start(); //開啟執行緒執行任務
workerStarted = true;
}
}
} finally {
if (! workerStarted)
addWorkerFailed(w);
}
return workerStarted;
}
工作執行緒的獲取任務
工作執行緒獲取任務的過程其實與阻塞佇列的任務出隊其實是相通的關係。這裡的獲取任務,換個容易理解的角度,就是讓執行緒自身不處於空閒狀態。
一個普普通通的Worker被建立的時候,它是分攜帶有 firstTask 和 不攜帶 firstTask兩種情況的。如果這個Worker已經有了firstTask
,那麼會首先解決(執行)掉,再去打阻塞佇列的主意。自身沒有攜帶 firstTask的Worker,就只能去不斷去呼叫 getTask()
從阻塞佇列取出任務來執行,這樣才能保持自身是非空閒狀態,才能免於被回收的命運。
一直獲取任務失敗會怎樣
非核心執行緒要在一定時間內獲取任務。因此某非核心執行緒在一定時間內無法獲取到任務,空閒狀態下的迴圈會結束,進入回收過程。
工作執行緒的執行任務
核心是runWorker(worker)
方法 ,真正執行任務的是工作執行緒所持有的執行緒示例
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;//獲取工作執行緒中用來執行任務的執行緒例項
w.firstTask = null;
w.unlock(); // 釋放鎖,允許執行緒被中斷
boolean completedAbruptly = true;
try {
while (task != null || (task = getTask()) != null) {//迴圈獲取任務
w.lock(); //上鎖
//如果池處於 STOP,請確保執行緒被中斷;
//如果沒有,請確保執行緒不被中斷。
//在第二種情況下需要再次檢查才能執行shutdownNow,然後執行中斷
if ((runStateAtLeast(ctl.get(), STOP) ||
(Thread.interrupted() && runStateAtLeast(ctl.get(), STOP) ) )
&& ! wt.isInterrupted())
wt.interrupt();//中斷當前執行緒
try {
beforeExecute(wt, task); //執行任務前回撥操作
Throwable thrown = null;
try {
task.run(); //執行任務task
} catch (RuntimeException x) {
thrown = x; throw x;
} catch (Error x) {
thrown = x; throw x;
} catch (Throwable x) {
thrown = x; throw new Error(x);
} finally {
afterExecute(task, thrown); //任務執行後回撥操作
}
} finally {
task = null; //清除任務引用
w.completedTasks++; //工作執行緒任務執行數+1
w.unlock(); //釋放鎖
}
}
completedAbruptly = false; //任務執行完畢,將 未完成標誌設為 false
} finally {
processWorkerExit(w, completedAbruptly);//退出獲取任務迴圈後進入回收
}
}
引用大佬畫的Worker執行任務流程圖:
工作執行緒的回收
執行緒池中執行緒的銷燬依賴JVM自動的回收,執行緒池做的工作是根據當前執行緒池的執行狀態和執行緒池種類維護對應數量的執行緒引用,並防止這部分執行緒被JVM回收,當執行緒池只需要將待回收的執行緒引用消除則視為回收。
Worker被建立出來後,會被呼叫runWorker(),就會不斷地進行輪詢從阻塞佇列中獲取任務去執行,核心執行緒可以無限等待獲取任務,非核心執行緒要限時獲取任務。當Worker在一定時間內無法獲取到任務,也就是獲取的任務為空時,空閒狀態下的迴圈會結束,Worker會執行processWorkerExit(),主動消除自身線上程池內的引用。
try {
while (task != null || (task = getTask()) != null) {
//省略執行任務程式碼
}
} finally {
processWorkerExit(w, completedAbruptly);//當一定時間內獲取不到任務時,主動回收自身
}
跟進去看看 processWorkerExit
方法。
private void processWorkerExit(Worker w, boolean completedAbruptly) {
if (completedAbruptly) // If abrupt, then workerCount wasn't adjusted
decrementWorkerCount(); //自減工作執行緒數
final ReentrantLock mainLock = this.mainLock;
mainLock.lock(); //可重入鎖,上鎖
try {
completedTaskCount += w.completedTasks; //記錄增加該工作執行緒的已完成的任務數
workers.remove(w); //將阻塞佇列中的w移除
} finally {
mainLock.unlock();
}
tryTerminate();//終止執行緒
int c = ctl.get();
if (runStateLessThan(c, STOP)) {//若執行狀態在STOP之下
if (!completedAbruptly) {
int min = allowCoreThreadTimeOut ? 0 : corePoolSize;
if (min == 0 && ! workQueue.isEmpty())
min = 1;
if (workerCountOf(c) >= min)
return; //工作執行緒數大於所需執行緒最小值,不需要取代
}
addWorker(null, false);//建立新的工作執行緒
}
}
由於執行緒池中執行緒的銷燬依賴JVM自動地回收,當執行緒引用被移出執行緒池管轄範圍時就已經結束了
再來看看這個 tryTerminate()
,其實這個方法功能如其名,嘗試去終止執行緒池。怎麼嘗試呢?首先要判斷當前執行緒池狀態和當前活躍工作執行緒數,滿足以下條件其中一個,都不會執行terminated(),也就不會執行緒池進入TERMINATED狀態。
- 執行緒池狀態為RUNNING
- 在TIDYING及之上
- SHUTDOWN且阻塞佇列仍有任務
- 當前活躍工作執行緒數不為0
final void tryTerminate() {
for (;;) {
int c = ctl.get();
if (isRunning(c) ||
runStateAtLeast(c, TIDYING) ||
(runStateOf(c) == SHUTDOWN && ! workQueue.isEmpty()))
return;
if (workerCountOf(c) != 0) { // Eligible to terminate
interruptIdleWorkers(ONLY_ONE);
return;
}
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {//通過CAS自旋判斷直到當前執行緒池執行狀態為TIDYING以及活躍執行緒數要為0
if (ctl.compareAndSet(c, ctlOf(TIDYING, 0))) {
try {
terminated();
} finally {
ctl.set(ctlOf(TERMINATED, 0));//更新執行緒池狀態和執行緒數
termination.signalAll();
}
return;
}
} finally {
mainLock.unlock();
}
// else retry on failed CAS
}
}
四、總結
再次借用大佬的圖,感謝感謝~
執行緒池機制設計的高度概括
執行緒池的設計分成三個管理模組:核心狀態管理、任務管理、執行緒管理,管理操作都有對執行緒池執行狀態、活躍執行緒數、阻塞佇列這些執行緒池的核心狀態來進行判斷,在不同的核心狀態下執行緒池會有不同的反應。
執行緒池通過維護一個32位的二進位制變數,前3位的高位儲存五種不同的執行緒池執行狀態,後29位低位儲存活躍執行緒數,為其他操作提供方便獲取狀態。
當外界通過execute()提交任務進執行緒池後,執行緒池會根據執行緒池的狀態與執行緒數,判斷該任務的命運:
- 建立新的工作執行緒執行該任務
- 緩衝到佇列中,等待工作執行緒申請執行任務
- 拒絕執行任務,執行拒絕策略
執行緒池使用了一個內部類——工作執行緒,會維護指定數量的工作執行緒引用,任務進入執行緒池內部後執行緒池會根據核心屬性對執行緒數量進行管理,當執行緒執行完獲取到的任務後則會繼續獲取新的任務去執行直到佇列已空。當某個工作執行緒在一定時間內都處於獲取不到任務的空閒狀態,執行緒池將它回收。
五、參考資料
- [1] JDK 1.8原始碼
- [2] Java執行緒池實現原理及其在美團業務中的實踐