在上一篇文章《從0到1玩轉執行緒池》中,我們瞭解了執行緒池的使用方法,以及向執行緒池中提交任務的完整流程和ThreadPoolExecutor.execute
方法的原始碼。在這篇文章中,我們將會從頭閱讀執行緒池ThreadPoolExecutor
類的原始碼,深入剖析執行緒池從提交任務到執行任務的完整流程,從而建立起完整的執行緒池執行模型。
檢視JDK原始碼的方式
在IDE中,例如IDEA裡,我們可以點選我們樣例程式碼裡的ThreadPoolExecutor
類跳轉到JDK中ThreadPoolExecutor
類的原始碼。在原始碼中我們可以看到很多java.util.concurrent
包的締造者大牛“Doug Lea”所留下的各種註釋,下面的圖片就是該類原始碼的一個截圖。
這些註釋的內容非常有參考價值,建議有能力的讀者朋友可以自己閱讀一遍。下面,我們就開始閱讀ThreadPoolExecutor
的原始碼吧。
控制變數與執行緒池生命週期
在ThreadPoolExecutor
類定義的開頭,我們可以看到如下的幾行程式碼:
// 控制變數,前3位表示狀態,剩下的資料位表示有效的執行緒數
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
// Integer的位數減去3位狀態位就是執行緒數的位數
private static final int COUNT_BITS = Integer.SIZE - 3;
// CAPACITY就是執行緒數的上限(含),即2^COUNT_BITS - 1個
private static final int CAPACITY = (1 << COUNT_BITS) - 1;
複製程式碼
第一行是一個用來作為控制變數的整型值,即一個Integer。之所以要用AtomicInteger
類是因為要保證多執行緒安全,在本系列之後的文章中會對AtomicInteger
進行具體介紹。一個整型一般是32位,但是這裡的程式碼為了保險起見,還是使用了Integer.SIZE
來表示整型的總位數。這裡的“位”指的是資料位(bit),在計算機中,8bit = 1位元組,1024位元組 = 1KB,1024KB = 1MB。每一位都是一個0或1的數字,我們如果把整型想象成一個二進位制(0或1)的陣列,那麼一個Integer就是32個數字的陣列。其中,前三個被用來表示狀態,那麼我們就可以表示2^3 = 8個不同的狀態了。剩下的29位二進位制數字都會被用於表示當前執行緒池中有效執行緒的數量,上限就是(2^29 - 1)個,即常量CAPACITY
。
之後的部分列出了執行緒池的所有狀態:
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;
複製程式碼
在這裡可以忽略數字後面的<< COUNT_BITS
,可以把狀態簡單地理解為前面的數字部分,這樣的簡化基本不影響結論。
各個狀態的解釋如下:
- RUNNING,正常執行狀態,可以接受新的任務和處理佇列中的任務
- SHUTDOWN,關閉中狀態,不能接受新任務,但是可以處理佇列中的任務
- STOP,停止中狀態,不能接受新任務,也不處理佇列中的任務,會中斷進行中的任務
- TIDYING,待結束狀態,所有任務已經結束,執行緒數歸0,進入TIDYING狀態後將會執行
terminated()
方法 - TERMINATED,結束狀態,
terminated()
方法呼叫完成後進入
這幾個狀態所對應的數字值是按照順序排列的,也就是說執行緒池的狀態只能從小到大變化,這也方便了通過數字比較來判斷狀態所在的階段,這種通過數字大小來比較狀態值的方法在ThreadPoolExecutor
的原始碼中會有大量的使用。
下圖是這五個狀態之間的變化過程:
- 當執行緒池被建立時會處於RUNNING狀態,正常接受和處理任務;
- 當
shutdown()
方法被直接呼叫,或者線上程池物件被GC回收時通過finalize()
方法隱式呼叫了shutdown()
方法時,執行緒池會進入SHUTDOWN狀態。該狀態下執行緒池仍然會繼續執行完阻塞佇列中的任務,只是不再接受新的任務了。當佇列中的任務被執行完後,執行緒池中的執行緒也會被回收。當佇列和執行緒都被清空後,執行緒池將進入TIDYING狀態; - 線上程池處於RUNNING或者SHUTDOWN狀態時,如果有程式碼呼叫了
shutdownNow()
方法,則執行緒池會進入STOP狀態。在STOP狀態下,執行緒池會直接清空阻塞佇列中待執行的任務,然後中斷所有正在進行中的任務並回收執行緒。當執行緒都被清空以後,執行緒池就會進入TIDYING狀態; - 當執行緒池進入TIDYING狀態時,將會執行
terminated()
方法,該方法執行完後,執行緒池就會進入最終的TERMINATED狀態,徹底結束。
到這裡我們就已經清楚地瞭解了執行緒從剛被建立時的RUNNING狀態一直到最終的TERMINATED狀態的整個生命週期了。那麼當我們要向一個RUNNING狀態的執行緒池提交任務時會發生些什麼呢?
execute方法的實現
我們一般會使用execute
方法提交我們的任務,那麼執行緒池在這個過程中做了什麼呢?在ThreadPoolExecutor
類的execute()
方法的原始碼中,我們主要做了四件事:
- 如果當前執行緒池中的執行緒數小於核心執行緒數corePoolSize,則通過threadFactory建立一個新的執行緒,並把入參中的任務作為第一個任務傳入該執行緒;
- 如果當前執行緒池中的執行緒數已經達到了核心執行緒數corePoolSize,那麼就會通過阻塞佇列workerQueue的
offer
方法來將任務新增到佇列中儲存,並等待執行緒空閒後進行執行; - 如果執行緒數已經達到了corePoolSize且阻塞佇列中無法插入該任務(比如已滿),那麼執行緒池就會再增加一個執行緒來執行該任務,除非執行緒數已經達到了最大執行緒數maximumPoolSize;
- 如果確實已經達到了最大執行緒數,那麼就會通過拒絕策略物件handler拒絕這個任務。
總體上的執行流程如下,下方的黑色同心圓代表流程結束:
這裡解釋一下阻塞佇列的定義,方便大家閱讀:
執行緒池中的阻塞佇列專門用於存放需要等待執行緒空閒的待執行任務,而阻塞佇列是這樣的一種資料結構,它是一個佇列(類似於一個List),可以存放0到N個元素。我們可以對這個佇列進行插入和彈出元素的操作,彈出操作可以理解為是一個獲取並從佇列中刪除一個元素的操作。當佇列中沒有元素時,對這個佇列的獲取操作將會被阻塞,直到有元素被插入時才會被喚醒;當佇列已滿時,對這個佇列的插入操作將會被阻塞,直到有元素被彈出後才會被喚醒。
這樣的一種資料結構非常適合於執行緒池的場景,當一個工作執行緒沒有任務可處理時就會進入阻塞狀態,直到有新任務提交後才被喚醒。
執行緒池中常用的阻塞佇列一般有三種型別:直連佇列、無界佇列、有界佇列。不同的阻塞佇列型別會被執行緒池的行為產生不同的影響,有興趣的讀者可以在上一篇文章《從0到1玩轉執行緒池》中找到不同型別阻塞佇列的具體解釋。
下面是帶有註釋的原始碼,大家可以和上面的流程對照起來參考一下:
public void execute(Runnable command) {
// 檢查提交的任務是否為空
if (command == null)
throw new NullPointerException();
// 獲取控制變數值
int c = ctl.get();
// 檢查當前執行緒數是否達到了核心執行緒數
if (workerCountOf(c) < corePoolSize) {
// 未達到核心執行緒數,則建立新執行緒
// 並將傳入的任務作為該執行緒的第一個任務
if (addWorker(command, true))
// 新增執行緒成功則直接返回,否則繼續執行
return;
// 因為前面呼叫了耗時操作addWorker方法
// 所以執行緒池狀態有可能發生了改變,重新獲取狀態值
c = ctl.get();
}
// 判斷執行緒池當前狀態是否是執行中
// 如果是則呼叫workQueue.offer方法將任務放入阻塞佇列
if (isRunning(c) && workQueue.offer(command)) {
// 因為執行了耗時操作“放入阻塞佇列”,所以重新獲取狀態值
int recheck = ctl.get();
// 如果當前狀態不是執行中,則將剛才放入阻塞佇列的任務拿出,如果拿出成功,則直接拒絕這個任務
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
// 如果執行緒池中沒有執行緒了,那就建立一個
addWorker(null, false);
}
// 如果放入阻塞佇列失敗(如佇列已滿),則新增一個執行緒
else if (!addWorker(command, false))
// 如果新增執行緒失敗(如已經達到了最大執行緒數),則拒絕任務
reject(command);
}
複製程式碼
從上面的原始碼中我們可以知道,當一個任務被通過ThreadPoolExecutor
的execute
方法提交到執行緒池中執行時,這個任務有可能以兩種方式被執行:
- 直接在建立一個新的Worker時被作為第一個任務傳入,由這個新建立的執行緒來執行;
- 把任務放入一個阻塞佇列,等待執行緒池中的工作執行緒Worker撈取任務進行執行。
這裡的這個Worker指的就是ThreadPoolExecutor.Worker
類,這是一個ThreadPoolExecutor
的內部類,用於對基礎執行緒類Thread
進行包裝和對執行緒進行管理。那麼執行緒池到底是怎麼利用Worker
類來實現持續不斷地接收提交的任務並執行的呢?接下來,我們通過ThreadPoolExecutor
的原始碼來一步一步抽絲剝繭,揭開執行緒池執行模型的神祕面紗。
addWorker方法
在上文中的execute
方法的程式碼中我們可以看到執行緒池是通過addWorker
方法來向執行緒池中新增新執行緒的,那麼新的執行緒又是如何執行起來的呢?
這裡我們暫時跳過addWorker
方法的詳細原始碼,因為雖然這個方法的程式碼行數較多,但是功能相對比較直接,只是通過new Worker(firstTask)
建立了一個代表執行緒的Worker
物件,然後呼叫了這個物件所包含的Thread
物件的start()
方法。
我們知道一旦呼叫了Thread
類的start()
方法,則這個執行緒就會開始執行建立執行緒時傳入的Runnable
物件。從下面的Worker
類構造器原始碼可以看出,Worker
類正是把自己(this引用)傳入了執行緒的構造器當中,所以這個執行緒啟動後就會執行Worker
類的run()
方法了,而在Worker
的run()
方法中只執行了一行很簡單的程式碼runWorker(this)
。
Worker(Runnable firstTask) {
setState(-1); // inhibit interrupts until runWorker
this.firstTask = firstTask;
this.thread = getThreadFactory().newThread(this);
}
public void run() {
runWorker(this);
}
複製程式碼
runWorker方法的實現
我們看到執行緒池中的執行緒在啟動時會呼叫對應的Worker
類的runWorker
方法,而這裡就是整個執行緒池任務執行的核心所在了。runWorker
方法中包含有一個類似無限迴圈的while語句,讓worker物件可以一直持續不斷地執行提交到執行緒池中的新任務或者等待下一個新任務的提交。
大家可以配合程式碼上帶有的註釋來理解該方法的具體實現:
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;
w.firstTask = null;
// 將worker的狀態重置為正常狀態,因為state狀態值在構造器中被初始化為-1
w.unlock();
// 通過completedAbruptly變數的值判斷任務是否正常執行完成
boolean completedAbruptly = true;
try {
// 如果task為null就通過getTask方法獲取阻塞佇列中的下一個任務
// getTask方法一般不會返回null,所以這個while類似於一個無限迴圈
// worker物件就通過這個方法的持續執行來不斷處理新的任務
while (task != null || (task = getTask()) != null) {
// 每一次任務的執行都必須獲取鎖來保證下方臨界區程式碼的執行緒安全
w.lock();
// 如果狀態值大於等於STOP(狀態值是有序的,即STOP、TIDYING、TERMINATED)
// 且當前執行緒還沒有被中斷,則主動中斷執行緒
if ((runStateAtLeast(ctl.get(), STOP) ||
(Thread.interrupted() &&
runStateAtLeast(ctl.get(), STOP))) &&
!wt.isInterrupted())
wt.interrupt();
// 開始
try {
// 執行任務前處理操作,預設是一個空實現
// 在子類中可以通過重寫來改變任務執行前的處理行為
beforeExecute(wt, task);
// 通過thrown變數儲存任務執行過程中丟擲的異常
// 提供給下面finally塊中的afterExecute方法使用
Throwable thrown = null;
try {
// *** 重要:實際執行任務的程式碼
task.run();
} catch (RuntimeException x) {
thrown = x; throw x;
} catch (Error x) {
thrown = x; throw x;
} catch (Throwable x) {
// 因為Runnable介面的run方法中不能丟擲Throwable物件
// 所以要包裝成Error物件丟擲
thrown = x; throw new Error(x);
} finally {
// 執行任務後處理操作,預設是一個空實現
// 在子類中可以通過重寫來改變任務執行後的處理行為
afterExecute(task, thrown);
}
} finally {
// 將迴圈變數task設定為null,表示已處理完成
task = null;
// 累加當前worker已經完成的任務數
w.completedTasks++;
// 釋放while體中第一行獲取的鎖
w.unlock();
}
}
// 將completedAbruptly變數設定為false,表示任務正常處理完成
completedAbruptly = false;
} finally {
// 銷燬當前的worker物件,並完成一些諸如完成任務數量統計之類的輔助性工作
// 線上程池當前狀態小於STOP的情況下會建立一個新的worker來替換被銷燬的worker
processWorkerExit(w, completedAbruptly);
}
}
複製程式碼
在runWorker
方法的原始碼中有兩個比較重要的方法呼叫,一個是while條件中對getTask
方法的呼叫,一個是在方法的最後對processWorkerExit
方法的呼叫。下面是對這兩個方法更詳細的解釋。
getTask
方法在阻塞佇列中有待執行的任務時會從佇列中彈出一個任務並返回,如果阻塞佇列為空,那麼就會阻塞等待新的任務提交到佇列中直到超時(在一些配置下會一直等待而不超時),如果在超時之前獲取到了新的任務,那麼就會將這個任務作為返回值返回。所以一般getTask
方法是不會返回null的,只會阻塞等待下一個任務並在之後將這個新任務作為返回值返回。
當getTask
方法返回null時會導致當前Worker退出,當前執行緒被銷燬。在以下情況下getTask
方法才會返回null:
- 當前執行緒池中的執行緒數超過了最大執行緒數。這是因為執行時通過呼叫
setMaximumPoolSize
修改了最大執行緒數而導致的結果; - 執行緒池處於STOP狀態。這種情況下所有執行緒都應該被立即回收銷燬;
- 執行緒池處於SHUTDOWN狀態,且阻塞佇列為空。這種情況下已經不會有新的任務被提交到阻塞佇列中了,所以執行緒應該被銷燬;
- 執行緒可以被超時回收的情況下等待新任務超時。執行緒被超時回收一般有以下兩種情況:
- 超出核心執行緒數部分的執行緒等待任務超時
- 允許核心執行緒超時(執行緒池配置)的情況下執行緒等待任務超時
processWorkerExit
方法會銷燬當前執行緒對應的Worker物件,並執行一些累加總處理任務數等輔助操作,但線上程池當前狀態小於STOP的情況下會建立一個新的Worker來替換被銷燬的Worker。
對getTask
和processWorkerExit
方法原始碼感興趣的讀者可以閱讀下一節來具體瞭解一下,不過跳過這一節也是完全可以的。
getTask與processWorkerExit方法原始碼
以下是getTask
與processWorkerExit
兩個方法的帶有中文解釋的原始碼:
private Runnable getTask() {
// 通過timeOut變數表示執行緒是否空閒時間超時了
boolean timedOut = false;
// 無限迴圈
for (;;) {
// 獲取執行緒池狀態
int c = ctl.get();
int rs = runStateOf(c);
// Check if queue empty only if necessary.
// 如果 執行緒池狀態>=STOP
// 或者 (執行緒池狀態==SHUTDOWN && 阻塞佇列為空)
// 則直接減少一個worker計數並返回null(返回null會導致當前worker被銷燬)
if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
decrementWorkerCount();
return null;
}
// 獲取執行緒池中的worker計數
int wc = workerCountOf(c);
// 判斷當前執行緒是否會被超時銷燬
// 會被超時銷燬的情況:執行緒池允許核心執行緒超時 或 當前執行緒數大於核心執行緒數
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
// 如果 (當前執行緒數大於最大執行緒數 或 (允許超時銷燬 且 當前發生了空閒時間超時))
// 且 (當前執行緒數大於1 或 阻塞佇列為空) —— 該條件在阻塞佇列不為空的情況下保證至少會保留一個執行緒繼續處理任務
// 則 減少worker計數並返回null(返回null會導致當前worker被銷燬)
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;
// 如果任務為null,則說明發生了等待超時,將空閒時間超時標誌設定為true
timedOut = true;
} catch (InterruptedException retry) {
// 如果等待被中斷了,那說明空閒時間(等待任務的時間)還沒有超時
timedOut = false;
}
}
}
複製程式碼
processWorkerExit方法的原始碼:
private void processWorkerExit(Worker w, boolean completedAbruptly) {
// 如果completedAbruptly為true則表示任務執行過程中丟擲了未處理的異常
// 所以還沒有正確地減少worker計數,這裡需要減少一次worker計數
if (completedAbruptly)
decrementWorkerCount();
// 獲取執行緒池的主鎖
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
// 把將被銷燬的執行緒已完成的任務數累計到執行緒池的完成任務總數上
completedTaskCount += w.completedTasks;
// 從worker集合中去掉將會銷燬的worker
workers.remove(w);
} finally {
// 釋放執行緒池主鎖
mainLock.unlock();
}
// 嘗試結束執行緒池
// 這裡是為了在關閉執行緒池時等到所有worker都被回收後再結束執行緒池
tryTerminate();
int c = ctl.get();
// 如果執行緒池狀態 < STOP,即RUNNING或SHUTDOWN
// 則需要考慮建立新執行緒來代替被銷燬的執行緒
if (runStateLessThan(c, STOP)) {
// 如果worker是正常執行完的,則要判斷一下是否已經滿足了最小執行緒數要求
// 否則直接建立替代執行緒
if (!completedAbruptly) {
// 如果允許核心執行緒超時則最小執行緒數是0,否則最小執行緒數等於核心執行緒數
int min = allowCoreThreadTimeOut ? 0 : corePoolSize;
// 如果阻塞佇列非空,則至少要有一個執行緒繼續執行剩下的任務
if (min == 0 && ! workQueue.isEmpty())
min = 1;
// 如果當前執行緒數已經滿足最小執行緒數要求
// 那麼就不建立替代執行緒了
if (workerCountOf(c) >= min)
return;
}
// 重新建立一個worker來代替被銷燬的執行緒
addWorker(null, false);
}
}
複製程式碼
總結
到這裡我們的執行緒池原始碼之旅就結束了,在這篇文章中我們首先了解了執行緒池中的控制變數與狀態變換流程,之後我們通過執行緒池的原始碼深入解析了從提交任務到執行任務的全過程,相信通過這些知識我們已經可以在腦海中建立起一套完整的執行緒池執行模型了。如果大家有一些細節感覺還不是特別清晰的話,建議不妨再返回到文章的開頭多讀幾遍,相信第二遍的閱讀能給大家帶來不一樣的體驗,因為我自己也是在第三次讀ThreadPoolExecutor
類的原始碼時才真正打通了其中的一些重要關節的。
引子
在瀏覽ThreadPoolExexutor
原始碼的過程中,有幾個點我們其實並沒有完全說清楚,比如對鎖的加鎖操作、對控制變數的多次獲取、控制變數的AtomicInteger型別。在下一篇文章中,我將會介紹這些以鎖、volatile變數、CAS操作、AQS抽象類為代表的一系列執行緒同步方法,歡迎感興趣的讀者繼續關注我後續釋出的文章~