目前,多線程編程可以說是在大部分平臺和應用上都需要實現的一個基本需求。本系列文章就來對 Java 平臺下的多線程編程知識進行講解,從概念入門、底層實現到上層應用都會涉及到,預計一共會有五篇文章,希望對你有所幫助😎😎
本篇文章是第五篇,應該也是最後一篇了,從現實需求出發到源碼介紹,一步步理清楚線程池的作用和優勢
線程池(ThreadPool)面對的是外部複雜多變的多線程環境,既需要保證多線程環境下的狀態同步,也需要最大化對每個線程的利用率,還需要留給子類足夠多的餘地來實現功能擴展。所以說,線程池的難點在於如何實現,而在概念上其實還是挺簡單的。在 Java 中,線程池這個概念一般都認爲對應的是 JDK 中的 ThreadPoolExecutor 類及其各種衍生類,本篇文章就從實現思路出發,探索 ThreadPoolExecutor 的源碼到底是如何實現的以及爲什麼這麼實現
一、線程池
線程是一種昂貴的系統資源,其“昂貴”不僅在於創建線程所需要的資源開銷,還在於使用過程中帶來的資源消耗。一個系統能夠支持同時運行的線程總數受限於該系統所擁有的處理器數目和內存大小等硬件條件,線程的運行需要佔用處理器時間片,系統中處於運行狀態的線程越多,每個線程單位時間內能分配到的時間片就會越少,線程調度帶來的上下文切換的次數就會越多,最終導致處理器真正用於運算的時間就會越少。此外,在現實場景中一個程序在其整個生命週期內需要交由線程執行的任務數量往往是遠多於系統所能支持同時運行的最大線程數。基於以上原因,爲每個任務都創建一個線程來負責執行是不太現實的。那麼,我們最直接的一個想法就是要考慮怎麼來實現線程的複用了
線程池就是實現線程複用的一種有效方式。線程池的思想可以看做是對資源是有限的而需要處理的任務幾乎是無限的這樣一個現狀的應對措施。線程池的一般實現思路是:線程池內部預先創建或者是先後創建一定數量的線程,外部將需要執行的任務作爲一個對象提交給線程池,由線程池選擇某條空閒線程來負責執行。如果所有線程都處於工作狀態且線程總數已經達到限制條件了,則先將任務緩存到任務隊列中,線程再不斷從任務隊列中取出任務並執行。因此,線程池可以看做是基於生產者-消費者模式的一種服務,內部維護的多個線程相當於消費者,提交的任務相當於產品,提交任務的外部就相當於生產者
二、思考下
好了,既然已經對線程池這個概念有了基本的瞭解,那麼就再來思考下線程池應該具備的功能以及應該如何來實現線程池
- 線程池中的線程最大數量應該如何限定?
既然我們不可能無限制地創建線程,那麼在創建線程池前就需要爲其設定一個最大數量,我們稱之爲最大線程池大小(maximumPoolSize),當線程池中的當前線程總數達到 maximumPoolSize 後就不應該再創建線程了。在開發中,我們需要根據運行設備的硬件條件和任務類型(I/O 密集型或者 CPU 密集型)來實際衡量該數值的大小,但任務的提交頻率和任務的所需執行時間是不固定的,所以線程池的 maximumPoolSize 也應該支持動態調整
- 線程池中的線程應該在什麼時候被創建呢?
一般來說,如果線程池中的線程數量還沒有達到 maximumPoolSize 時,我們可以等到當外部提交了任務時再來創建線程進行處理。但是,線程從被創建到被調度器選中運行,之間也是有着一定時間間隔的。從提高任務的處理響應速度這方面考慮,我們也可以選擇預先就創建一批線程進行等待
- 線程池中的線程可以一直存活着嗎?
程序運行過程中可能只是偶發性地大批量提交任務,而大部分時間只是比較零散地提交少量任務,這就導致線程池中的線程可能會在一段時間內處於空閒狀態。如果線程池中的線程只要創建了就可以一直存活着的話,那麼線程池的“性價比”就顯得沒那麼高了。所以,當線程處於空閒狀態的時間超出允許的最大空閒時間(keepAliveTime)後,我們就應該將其回收,避免白白浪費系統資源。而又爲了避免頻繁地創建和銷燬線程,線程池需要緩存一定數量的線程,即使其處於空閒狀態也不會進行回收,這類線程我們就稱之爲核心線程,相應的線程數量就稱之爲核心線程池大小(corePoolSize)。大於 corePoolSize 而小於等於 maximumPoolSize 的那一部分線程,就稱之爲非核心線程
- 如何實現線程的複用?
我們知道,當 Thread.run()
方法執行結束後線程就會被回收了,那麼想要實現線程的複用,那麼就要考慮如何避免退出 Thread.run()
了。這裏,我們可以通過循環向任務隊列取值的方式來實現。上面有提到,如果外部提交的任務過多,那麼任務就需要被緩存到任務隊列中。那麼,我們就可以考慮使用一個阻塞隊列來存放任務。線程循環從任務隊列中取任務,如果隊列不爲空,那麼就可以馬上拿到任務進行執行;如果隊列爲空,那麼就讓線程一直阻塞等待,直到外部提交了任務被該線程拿到或者由於超時退出循環。通過這種循環獲取+阻塞等待的方式,就可以實現線程複用的目的
- 如何儘量實現線程的複用?
這個問題和“如何實現線程的複用”不太一樣,“如何實現線程的複用”針對的是單個線程的複用流程,本問題針對的是整個線程池範圍的複用。線程池中需要使用到任務隊列進行緩存,那麼任務隊列的使用時機可以有以下幾種:
- 當線程數已經達到 maximumPoolSize ,且所有線程均處於工作狀態時,此後外部提交的任務才被緩存到任務隊列中
- 當核心線程都已經被創建了時,此後外部提交的任務就被緩存到任務隊列中,當任務隊列滿了後才創建非核心線程來循環處理任務
很明顯的,第二種方案會更加優秀。由於核心線程一般情況下是會被長久保留的,核心線程的存在保證了外部提交的任務一直有在被循環處理。如果外部提交的大部分都是耗時較短的任務或者任務的提交頻率比較低的話,那麼任務隊列就可能沒那麼容易滿,第二種方案就可以儘量避免去創建非核心線程。而且對於“偶發性地大批量提交任務,而大部分時間只是比較零散地提交少量任務”這種情況,第二種方案也會更加合適。當然,在任務的處理速度方面,第一種方案就會高一些,但是如果想要儘量提高第二種方案的任務處理速度的話,也可以通過將任務隊列的容量調小的方式來實現
- 當任務隊列滿了後該如何處理?
如果線程池實在“忙不過來”的話,那麼任務隊列也是有可能滿的,那麼就需要爲這種情況指定處理策略。當然,我們也可以選擇使用一個無界隊列來緩存任務,但是無界隊列容易掩蓋掉一些程序異常。因爲有界隊列之所以會滿,可能是由於發生線程池死鎖或者依賴的某個基礎服務失效導致的,從而令線程池中的任務一直遲遲得不到解決。如果使用的是無界隊列的話,就可能使得當系統發生異常時程序還是看起來運轉正常,從而降低了系統健壯性。所以,最常用的還是有界隊列
現實需求是多樣化的,在實現線程池時就需要留有交由外部自定義處理策略的餘地。例如,當隊列滿了後,我們可以選擇直接拋出異常來向外部“告知”這一異常情況。對於重要程度較低的任務,可以選擇直接拋棄該任務,也可以選擇拋棄隊列頭的任務而嘗試接納新到來的任務。如果任務必須被執行的話,也可以直接就在提交任務的線程上進行執行
以上就是線程池在實現過程中需要主要考慮的幾個點,下面就來看下 Java 實際上是怎麼實現線程池的
三、ThreadPoolExecutor
java.util.concurrent.ThreadPoolExecutor
類就是 Java 對線程池的默認實現,下文如果沒有特別說明的話,所說的線程池就是指 ThreadPoolExecutor
ThreadPoolExecutor 的繼承關係如下圖所示
ThreadPoolExecutor 實現的最頂層接口是 Executor。Executor 提供了一種將任務的提交和任務的執行兩個操作進行解耦的思路:客戶端無需關注執行任務的線程是如何創建、運行和回收的,只需要將任務的執行邏輯包裝爲一個 Runnable 對象傳遞進來即可,由 Executor 的實現類自己來完成最複雜的執行邏輯
ExecutorService 接口在 Executor 的基礎上擴展了一些功能:
- 擴展執行任務的能力。例如:獲取任務的執行結果、取消任務等功能
- 提供了關閉線程池、停止線程池,以及阻塞等待線程池完全終止的方法
AbstractExecutorService 則是上層的抽象類,負責將任務的執行流程串聯起來,從而使得下層的實現類 ThreadPoolExecutor 只需要實現一個執行任務的方法即可
也正如上文所說的那樣,ThreadPoolExecutor 可以看做是基於生產者-消費者模式的一種服務,內部維護的多個線程相當於消費者,提交的任務相當於產品,提交任務的外部就相當於生產者。其整個運行流程如下圖所示
而在線程池的整個生命週期中,以下三個關於線程數量的統計結果也影響着線程池的流程走向
- 當前線程池大小(currentPoolSize)。表示當前實時狀態下線程池中線程的數量
- 最大線程池大小(maximumPoolSize)。表示線程池中允許存在的線程的數量上限
- 核心線程池大小(corePoolSize)。表示一個不大於 maximumPoolSize 的線程數量上限
三者之間的關係如下:
當前線程池大小 ≤ 核心線程池大小 ≤ 最大線程池大小
or
核心線程池大小 ≤ 當前線程池大小 ≤ 最大線程池大小
當中,除了“當前線程池大小”是對線程池現有的工作者線程進行實時計數的結果,其它兩個值都是對線程池配置的參數值。三個值的作用在上文也都已經介紹了
ThreadPoolExecutor 中參數最多的一個構造函數的聲明如下所示:
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;
}
- corePoolSize :用於指定線程池的核心線程數大小
- maximumPoolSize:用於指定最大線程池大小
- keepAliveTime、unit :一起用於指定線程池中空閒線程的最大存活時間
- workQueue :任務隊列,相當於生產者-消費者模式中的傳輸管道,用於存放待處理的任務
- threadFactory:用於指定創建線程的線程工廠
- handler:用於指定當任務隊列已滿且線程數量達到 maximumPoolSize 時任務的處理策略
下面就以點見面,從細節處來把握整個線程池的流程走向
1、線程池的狀態如何保存
這裏所說的“狀態”指的是一個複合數據,包含“線程池生命週期狀態”和“線程池當前線程數量”這兩項。線程池從啓動到最終終止,其內部需要記錄其當前狀態來決定流程走向。而線程池的當前線程數量,也關乎着線程是否需要進行回收以及是否需要執行任務的拒絕策略
線程池一共包含以下五種生命週期狀態,涵蓋了線程池從啓動到終止的這整個範圍。線程池的生命週期狀態可以按順序躍遷,但無法反向迴轉,每個狀態的數值大小也是逐步遞增的
//運行狀態,線程池正處於運行中
private static final int RUNNING = -1 << COUNT_BITS;
//關閉狀態,當調用 shutdown() 方法後處於這個狀態,任務隊列中的任務會繼續處理,但不再接受新任務,
private static final int SHUTDOWN = 0 << COUNT_BITS;
//停止狀態,當調用 shutdownNow() 方法後處於這個狀態
//任務隊列中的任務也不再處理且作爲方法返回值返回,此後不再接受新任務
private static final int STOP = 1 << COUNT_BITS;
//TERMINATED 之前的臨時狀態,當線程都被回收且任務隊列已清空後就會處於這個狀態
private static final int TIDYING = 2 << COUNT_BITS;
//終止狀態,在處於 TIDYING 狀態後會立即調用 terminated() 方法,調用完成就會馬上轉到此狀態
private static final int TERMINATED = 3 << COUNT_BITS;
在日常開發中,如果我們想要用一個 int 類型的 state 變量來表示這五種狀態的話,那麼就可能是通過讓 state 分別取值 1,2,3,4,5 來進行標識,而 state 作爲一個 int 類型是一共有三十二位的,那其實上僅需要佔用三位就足夠標識了,即 2 x 2 x 2 = 8 種可能。那還剩下 29 位可以用來存放其它數據
實際上 ThreadPoolExecutor 就是通過將一個 32 位的 int 類型變量分割爲兩段,高 3 位用來表示線程池的當前生命週期狀態,低 29 位就拿來表示線程池的當前線程數量,從而做到用一個變量值來維護兩份數據,這個變量值就是 ctl
。從 ctl
的初始值就可以知道線程池的初始生命週期狀態( runState )是 RUNNING
,初始線程數量 ( workerCount )是 0。這種用一個變量去存儲兩個值的做法,可以避免在做相關決策時出現不一致的情況,且不必爲了維護兩者的一致而使用鎖,後續需要獲取線程池的當前生命週期狀態和線程數量的時候,也可以直接採用位運算的方式獲取,在速度上相比基本運算會快很多
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
ThreadPoolExecutor 還聲明瞭以下兩個常量用來參與位運算
//用來表示線程數量的位數,即 29
private static final int COUNT_BITS = Integer.SIZE - 3;
//線程池所能表達的最大線程數,即一個“高3位全是0,低29位全是1”的數值
private static final int CAPACITY = (1 << COUNT_BITS) - 1;
相應的,那麼就需要有幾個方法可以來分別取“生命週期狀態”和“線程數”這兩個值,以及將這兩個值合併保存的方法,這幾個方法都使用到了位運算
// Packing and unpacking ctl
//通過按位取反 + 按位與運算,將 c 的高3位保留,捨棄低29位,從而得到線程池狀態
private static int runStateOf(int c) { return c & ~CAPACITY; }
//通過按位與運算,將 c 的高3位捨棄,保留低29位,從而得到工作線程數
private static int workerCountOf(int c) { return c & CAPACITY; }
//rs,即 runState,線程池的生命週期狀態
//wc,即 workerCount,工作線程數量
//通過按位或運算來合併值
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;
}
//用於判斷線程池是否處於 RUNNING 狀態
//由於五個狀態值的大小是依次遞增的,所以只需要和 SHUTDOWN 比較即可
private static boolean isRunning(int c) {
return c < SHUTDOWN;
}
public boolean isShutdown() {
return !isRunning(ctl.get());
}
//用於判斷當前狀態是否處於 SHUTDOWN、STOP、TIDYING 三者中的一個
public boolean isTerminating() {
int c = ctl.get();
return !isRunning(c) && runStateLessThan(c, TERMINATED);
}
//用於判斷當前狀態是否處於 TERMINATED
public boolean isTerminated() {
return runStateAtLeast(ctl.get(), TERMINATED);
}
2、線程的創建流程
在初始狀態下,客戶端每提交一個任務,線程池就會通過 threadFactory
創建線程來處理該任務,如果開發者沒有指定 threadFactory
的話,則會使用 Executors.DefaultThreadFactory
。線程池在最先開始創建的線程屬於核心線程,線程數量在大於 corePoolSize 而小於等於 maximumPoolSize 這個範圍內的線程屬於非核心線程。需要注意的是,核心線程和非核心線程並非是兩種不同類型的線程對象,這兩個概念只是對不同數量範圍內的線程進行的區分,實質上這兩者指向的都是同一類型
線程的創建流程可以通過任務的提交流程來了解,任務的提交流程圖如下所示
線程池開放了多個讓外部提交任務的方法,這裏主要看 execute(Runnable command)
方法。該方法需要先後多次校驗狀態值,因爲線程池面對的調用方可以來自於多個不同的線程。可能在當前線程提交任務的同時,其它線程就剛好關閉了線程池或者是調整了線程池的線程大小參數,需要考慮當前的線程數量是否已經達到限制了
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
if (workerCountOf(c) < corePoolSize) {
//如果當前線程數還未達到 corePoolSize,則嘗試創建一個核心線程來處理任務
//addWorker 可能會因爲線程池被關閉了、線程數量超出限制等原因返回 false
if (addWorker(command, true))
return;
c = ctl.get();
}
if (isRunning(c) && workQueue.offer(command)) { //線程池還處於運行狀態且成功添加任務到任務隊列
//需要重新檢查下運行狀態
//因爲等執行到這裏時,線程池可能被其它線程關閉了
int recheck = ctl.get();
//1、如果線程池已經處於非運行狀態了
//1.1、如果移除 command 成功,則走拒絕策略
//1.2、如果移除 command 失敗(因爲 command 可能已經被其它線程拿去執行了),則走第 3 步
//2、如果線程池還處於運行狀態,則走第 3 步
//3、如果當前線程數量爲 0,則創建線程進行處理
//第 3 步的意義在於:corePoolSize 可以被設爲 0,所以這裏需要檢查下,在需要的時候創建一個非核心線程
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
//如果線程池處於非運行狀態了,或者是處於運行狀態但隊列已滿了,此時就會走到這裏
//在這裏嘗試創建一個非核心線程
//如果線程創建失敗,說明要麼是線程池當前狀態大於等於 STOP,或者是任務隊列已滿且線程總數達到 maximumPoolSize 了
//此時就走拒絕策略
else if (!addWorker(command, false))
reject(command);
}
當中,addWorker(Runnable firstTask, boolean core)
方法用於嘗試創建並啓動線程,同時將線程保存到 workers
。第一個參數用於指定線程啓動時需要執行的第一個任務,可以爲 null。第二個參數用於指定要創建的是否是核心線程,這個參數會關係到線程是否能被成功創建
該方法在實際創建線程前,都需要先通過 CAS 來更新(遞增加一)當前的線程總數,通過 for 循環來不斷進行重試。當 CAS 成功後,則會再來實際進行線程的創建操作。但在這時候線程也未必能夠創建成功,因爲在 CAS 成功後線程池可能被關閉了,或者是在創建線程時拋出異常了,此時就需要回滾對 workerCount 的修改
該方法如果返回 true,意味着新創建了一個 Worker 線程,同時線程也被啓動了
該方法如果返回 false,則可能是由於以下情況:
- 生命週期狀態大於等於 STOP
- 生命週期狀態等於 SHUTDOWN,但 firstTask 不爲 null,或者任務隊列爲空
- 當前線程數已經超出限制
- 符合創建線程的條件,但創建過程中或啓動線程的過程中拋出了異常
private boolean addWorker(Runnable firstTask, boolean core) {
//下面的 for 主要邏輯:
//在創建線程前通過 CAS 原子性地將“工作者線程數量遞增加一”
//由於 CAS 可能會失敗,所以將之放到 for 循環中進行循環重試
//每次循環前後都需要檢查下當前狀態是否允許創建線程
retry:
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);
// Check if queue empty only if necessary.
if (rs >= SHUTDOWN &&
! (rs == SHUTDOWN &&
firstTask == null &&
! workQueue.isEmpty()))
//當外部調用 shutdown() 方法後,線程池狀態會變遷爲 SHUTDOWN
//此時依然允許創建線程來對隊列中的任務進行處理,但是不會再接受新任務
//除這種情況之外不允許在非 RUNNING 的時候還創建線程
return false;
for (;;) {
int wc = workerCountOf(c);
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maximumPoolSize))
//當前線程數已經超出最大限制
return false;
if (compareAndIncrementWorkerCount(c))
//通過 CAS 更新工作者線程數成功後就跳出循環,去實際創建線程
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()) // precheck that t is startable
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;
}
3、線程的執行流程
上面所講的線程其實指的是 ThreadPoolExecutor 的內部類 Worker ,Worker 內部包含了一個 Thread 對象,所以本文就把 Worker 實例也看做線程來對待
Worker 繼承於 AbstractQueuedSynchronizer,意味着 Worker 就相當於一個鎖。之沒有使用 synchronized 或者 ReentrantLock,是因爲它們都是可重入鎖,Worker 繼承於 AQS 爲的就是自定義實現不可重入的特性來輔助判斷線程是否處於執行任務的狀態:在開始執行任務前進行加鎖,在任務執行結束後解鎖,以便在後續通過判斷 Worker 是否處於鎖定狀態來得知其是否處於執行階段
- Worker 在開始執行任務前會執行
Worker.lock()
,表明線程正在執行任務 - 如果 Worker 處於鎖定狀態,則不應該對其進行中斷,避免任務執行一半就被打斷
- 如果 Worker 處於非鎖定狀態,說明其當前是處於阻塞獲取任務的狀態,此時才允許對其進行中斷
- 線程池在執行
shutdown()
方法或shutdownNow()
方法時會調用interruptIdleWorkers()
方法來回收空閒的線程,interruptIdleWorkers()
方法會使用Worker.tryLock()
方法來嘗試獲取鎖,由於 Worker 是不可重入鎖,所以如果鎖獲取成功就說明線程處於空閒狀態,此時纔可以進行回收
Worker 同時也是 Runnable 類型,thread
是通過 getThreadFactory().newThread(this)
來創建的,即將 Worker 本身作爲構造參數傳給 Thread 進行初始化,所以在 thread
啓動的時候 Worker 的 run()
方法就會被執行
private final class Worker
extends AbstractQueuedSynchronizer
implements Runnable
{
/**
* This class will never be serialized, but we provide a
* serialVersionUID to suppress a javac warning.
*/
private static final long serialVersionUID = 6138294804551838833L;
/** Thread this worker is running in. Null if factory fails. */
final Thread thread;
//線程要執行的第一個任務,可能爲 null
/** Initial task to run. Possibly null. */
Runnable firstTask;
//用於標記 Worker 執行過的任務數(不管成功與否都記錄)
/** Per-thread task counter */
volatile long completedTasks;
/**
* Creates with given first task and thread from ThreadFactory.
* @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);
}
// Lock methods
//
// The value 0 represents the unlocked state.
// The value 1 represents the locked state.
protected boolean isHeldExclusively() {
return getState() != 0;
}
//只有在 state 值爲 0 的時候才能獲取到鎖,以此實現不可重入的特性
protected boolean tryAcquire(int unused) {
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
protected boolean tryRelease(int unused) {
setExclusiveOwnerThread(null);
setState(0);
return true;
}
public void lock() { acquire(1); }
public boolean tryLock() { return tryAcquire(1); }
public void unlock() { release(1); }
public boolean isLocked() { return isHeldExclusively(); }
//向線程發起中斷請求
void interruptIfStarted() {
Thread t;
if (getState() >= 0 && (t = thread) != null && !t.isInterrupted()) {
try {
t.interrupt();
} catch (SecurityException ignore) {
}
}
}
}
runWorker(Worker)
方法就是線程正式進行任務執行的地方。該方法通過 while 循環不斷從任務隊列中取出任務來進行執行,如果 getTask()
方法返回了 null,那此時就需要將此線程進行回收。如果在任務執行過程中拋出了異常,那也需要回收此線程
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;
w.firstTask = null;
//因爲 Worker 的默認值是 -1,而 Worker 的 interruptIfStarted() 方法只有在 state >=0 的時候才允許進行中斷
//所以這裏調用 unlock() 並不是爲了解鎖,而是爲了讓 Worker 的 state 值變爲 0,讓 Worker 允許外部進行中斷
//所以,即使客戶端調用了 shutdown 或者 shutdownNow 方法,在 Worker 線程還未執行到這裏前,無法在 interruptWorkers() 方法裏發起中斷請求
w.unlock(); // allow interrupts
//用於標記是否由於被打斷而非正常結束導致的線程終止
//爲 true 表示非正常結束
boolean completedAbruptly = true;
try {
// 如果 getTask() 爲 null,說明線程池已經被停止或者需要進行線程回收
while (task != null || (task = getTask()) != null) {
//在開始執行任務前進行加鎖,在任務執行結束後解鎖
//以便在後續通過判斷 Worker 是否處於鎖定狀態來得知其是否處於執行階段
w.lock();
// If pool is stopping, ensure thread is interrupted;
// if not, ensure thread is not interrupted. This
// requires a recheck in second case to deal with
// shutdownNow race while clearing interrupt
//確保當狀態值大於等於 STOP 時有向線程發起過中斷請求
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();
} 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++;
w.unlock();
}
}
completedAbruptly = false;
} finally {
//回收此線程
processWorkerExit(w, completedAbruptly);
}
}
private Runnable getTask() {
boolean timedOut = false; // Did the last poll() time out?
for (; ; ) {
int c = ctl.get();
int rs = runStateOf(c);
// Check if queue empty only if necessary.
//如何當前狀態大於等於 STOP,則返回 null
//如何當前狀態是 SHUTDOWN 且任務隊列爲空,則返回 null
if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
decrementWorkerCount();
return null;
}
int wc = workerCountOf(c);
// Are workers subject to culling?
//timed 用於標記從任務隊列中取任務時是否需要進行超時控制
//如果允許回收空閒核心線程或者是當前的線程總數已經超出 corePoolSize 了,那麼就需要進行超時控制
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
//1. 線程總數超出 maximumPoolSize
//2. 允許回收核心線程,且核心線程的空閒時間已達到限制了
//如果以上兩種情況之一有一個滿足,且當前線程數大於 1 或者任務隊列爲空時就返回 null(如果 CAS 更新 WorkerCount 成功的話)
//避免在任務隊列不爲空且只有一個線程時還回收線程導致任務沒人處理
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;
//如果 r 爲 null,說明是由於超時導致 poll 返回了 null
//在下一次循環時將判斷是否回收此線程
timedOut = true;
} catch (InterruptedException retry) {
timedOut = false;
}
}
}
getTask()
方法獲取任務的流程圖如下所示:
4、線程的回收流程
當外部調用了線程池的以下幾個方法之一時,就會觸發到線程的回收機制:
- 允許回收核心線程:allowCoreThreadTimeOut()
- 重置核心線程池大小:setCorePoolSize()
- 重置最大線程池大小:setMaximumPoolSize()
- 重置線程最大空閒時間:setKeepAliveTime()
- 關閉線程池:shutdown()
- 停止線程池:shutdownNow()
/**
* 用於控制核心線程是否可以由於空閒時間超時而被回收
* 超時時間和非核心線程一樣由 keepAliveTime 來指定
*
* @param value
*/
public void allowCoreThreadTimeOut(boolean value) {
if (value && keepAliveTime <= 0)
throw new IllegalArgumentException("Core threads must have nonzero keep alive times");
if (value != allowCoreThreadTimeOut) {
allowCoreThreadTimeOut = value;
if (value)
//回收掉空閒線程
interruptIdleWorkers();
}
}
/**
* 重置 corePoolSize
*
* @param corePoolSize
*/
public void setCorePoolSize(int corePoolSize) {
if (corePoolSize < 0)
throw new IllegalArgumentException();
int delta = corePoolSize - this.corePoolSize;
this.corePoolSize = corePoolSize;
if (workerCountOf(ctl.get()) > corePoolSize)
//如果當前的線程總數已經超出新的 corePoolSize 的話那就進行線程回收
interruptIdleWorkers();
else if (delta > 0) {
//會走進這裏,說明新的 corePoolSize 比原先的大,但當前線程總數還小於等於新的 corePoolSize
//此時如果任務隊列不爲空的話,那麼就需要創建一批新的核心線程來處理任務
//delta 和 workQueueSize 中的最小值就是需要啓動的線程數
//而如果在創建過程中任務隊列已經空了(被其它線程拿去處理了),那就不再創建線程
int k = Math.min(delta, workQueue.size());
while (k-- > 0 && addWorker(null, true)) {
if (workQueue.isEmpty())
break;
}
}
}
/**
* 用於設置線程池允許存在的最大活躍線程數
*
* @param maximumPoolSize
*/
public void setMaximumPoolSize(int maximumPoolSize) {
if (maximumPoolSize <= 0 || maximumPoolSize < corePoolSize)
throw new IllegalArgumentException();
this.maximumPoolSize = maximumPoolSize;
if (workerCountOf(ctl.get()) > maximumPoolSize)
//回收掉空閒線程
interruptIdleWorkers();
}
/**
* 用於設置非核心線程在空閒狀態能夠存活的時間
*
* @param time
* @param unit
*/
public void setKeepAliveTime(long time, TimeUnit unit) {
if (time < 0)
throw new IllegalArgumentException();
//爲了避免頻繁創建線程,核心線程如果允許超時回收的話,超時時間不能爲 0
if (time == 0 && allowsCoreThreadTimeOut())
throw new IllegalArgumentException("Core threads must have nonzero keep alive times");
long keepAliveTime = unit.toNanos(time);
long delta = keepAliveTime - this.keepAliveTime;
this.keepAliveTime = keepAliveTime;
if (delta < 0) //如果新設置的值比原先的超時時間小的話,那就需要去回收掉空閒線程
interruptIdleWorkers();
}
/**
* 關閉線程池
*/
public void shutdown() {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
checkShutdownAccess();
//將當前狀態設置爲 SHUTDOWN
advanceRunState(SHUTDOWN);
//回收掉空閒線程
interruptIdleWorkers();
onShutdown(); // hook for ScheduledThreadPoolExecutor
} finally {
mainLock.unlock();
}
//嘗試看是否能把線程池狀態置爲 TERMINATED
tryTerminate();
}
/**
* 停止線程池
*
* @return 任務隊列中緩存的所有任務
*/
public List<Runnable> shutdownNow() {
List<Runnable> tasks;
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
checkShutdownAccess();
//將當前狀態設置爲 STOP
advanceRunState(STOP);
//回收掉空閒線程
interruptWorkers();
//獲取任務隊列中緩存的所有任務
tasks = drainQueue();
} finally {
mainLock.unlock();
}
//嘗試看是否能把線程池狀態置爲 TERMINATED
tryTerminate();
return tasks;
}
上述的幾個方法最終都會調用 interruptIdleWorkers(boolean onlyOne)
方法來回收空閒線程。該方法通過向線程發起中斷請求來使 Worker 退出 runWorker(Worker w)
方法,最終會調用 processWorkerExit(Worker w, boolean completedAbruptly)
方法來完成實際的線程回收操作
private void interruptIdleWorkers() {
interruptIdleWorkers(false);
}
private void interruptIdleWorkers(boolean onlyOne) {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
for (Worker w : workers) {
Thread t = w.thread;
//僅在線程的中斷標記爲 false 時才發起中斷,避免重複發起中斷請求
//且僅在 w.tryLock() 能成功(即 Worker 並非處於執行任務的階段)才發起中斷,避免任務還未執行完就被打斷
if (!t.isInterrupted() && w.tryLock()) {
try {
t.interrupt();
} catch (SecurityException ignore) {
} finally {
w.unlock();
}
}
if (onlyOne)
break;
}
} finally {
mainLock.unlock();
}
}
/**
* 回收線程
*
* @param w Worker
* @param completedAbruptly 是否是由於任務執行過程拋出異常導致需要來回收線程
* true:由於任務拋出異常
* false:由於線程空閒時間達到限制條件
*/
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);
} finally {
mainLock.unlock();
}
tryTerminate();
int c = ctl.get();
if (runStateLessThan(c, STOP)) {
//在任務隊列不爲空的時候,需要確保至少有一個線程可以來處理任務,否則就還是需要再創建一個新線程
if (!completedAbruptly) {
int min = allowCoreThreadTimeOut ? 0 : corePoolSize;
if (min == 0 && !workQueue.isEmpty())
min = 1;
if (workerCountOf(c) >= min)
return; // replacement not needed
}
addWorker(null, false);
}
}
除了上述幾個方法會主動觸發到線程回收機制外,當線程池滿足以下幾種情況之一時,也會進行線程的回收:
- 非核心線程的空閒時間超出了 keepAliveTime
- allowCoreThreadTimeOut 爲 true 且核心線程的空閒時間超出了 keepAliveTime
以上幾種情況其觸發時機主要看 getTask()
方法就可以。在向任務隊列 workQueue 獲取任務前,通過判斷當前線程池的 allowCoreThreadTimeOut、corePoolSize、workerCount
等參數來決定是否需要對“從任務隊列獲取任務”這個操作進行限時。如果需要進行限時且獲取任務的時間超出 keepAliveTime 的話,那就說明此線程的空閒時間已經達到限制了,需要對其進行回收
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?
//timed 用於標記從任務隊列中取任務時是否需要進行超時控制
//如果允許回收空閒核心線程或者是當前的線程總數已經超出 corePoolSize 了,那麼就需要進行超時控制
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
//1. 線程總數超出 maximumPoolSize
//2. 允許回收核心線程,且核心線程的空閒時間已達到限制了
//如果以上兩種情況之一有一個滿足,且當前線程數大於 1 或者任務隊列爲空時就返回 null(如果 CAS 更新 WorkerCount 成功的話)
//避免在任務隊列不爲空且只有一個線程時還回收線程導致任務沒人處理
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;
//如果能執行到 timedOut = true 說明是由於超時導致 poll 返回了 null
//之所以不在判斷到 r 不爲 null 的時候就直接 return 出去
//是因爲可能在獲取任務的過程中外部又重新修改了 allowCoreThreadTimeOut 和 corePoolSize 等配置
//導致此時又不需要回收此線程了,所以就在下一次循環時再判斷是否回收此線程
} catch (InterruptedException retry) {
timedOut = false;
}
}
}
以上就是線程池基本所有的線程回收流程。線程回收機制有助於節約系統資源,但如果 corePoolSize、keepAliveTime 等參數設置得和系統的實際運行情況不符的話,反而會導致線程頻繁地被創建和回收,反而加大了資源開銷
5、線程池的關閉流程
shutdown()
和 shutdownNow()
方法可以用來關閉和停止線程池
- shutdown()。使用該方法,已提交的任務會被繼續執行,而後續新提交的任務則會走拒絕策略。該方法返回時,線程池可能尚未走向終止狀態 TERMINATED,即線程池中可能還有線程還在執行任務
- shutdownNow()。使用該方法,正在運行的線程會嘗試停止,任務隊列中的任務也不會執行而是作爲方法返回值返回。由於該方法是通過調用
Thread.interrupt()
方法來停止正在執行的任務的,因此某些無法響應中斷的任務可能需要等到任務完成後才能停止線程
由於這兩個方法調用過後線程池都不會再接收新任務了,所以在回收空閒線程後,還需要檢查下線程是否都已經回收完畢了,是的話則需要將線程池的生命週期狀態向 TIDYING 和 TERMINATED 遷移
final void tryTerminate() {
for (;;) {
int c = ctl.get();
//在以下幾種情況不需要終止線程池:
//1.還處於運行狀態
//2.已經處於 TIDYING 或 TERMINATED 狀態
//3.處於 SHUTDOWN 狀態且還有待處理的任務
if (isRunning(c) ||
runStateAtLeast(c, TIDYING) ||
(runStateOf(c) == SHUTDOWN && ! workQueue.isEmpty()))
return;
//在達到 TIDYING 狀態前需要確保所有線程都被關閉了
if (workerCountOf(c) != 0) { // Eligible to terminate
interruptIdleWorkers(ONLY_ONE);
return;
}
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
if (ctl.compareAndSet(c, ctlOf(TIDYING, 0))) {
try {
//terminated() 方法執行完畢後,線程池狀態就從 TIDYING 轉爲 TERMINATED 了,此時線程池就走向終止了
terminated();
} finally {
ctl.set(ctlOf(TERMINATED, 0));
//喚醒所有在等待線程池 TERMINATED 的線程
termination.signalAll();
}
return;
}
} finally {
mainLock.unlock();
}
// else retry on failed CAS
}
}
6、任務隊列的選擇
阻塞隊列(BlockingQueue)是一個支持兩個附加操作的隊列。這兩個附加的操作是:在隊列爲空時,獲取數據的線程會阻塞等待直到從隊列獲取到任務。當隊列已滿時,存儲數據的線程會阻塞等待直到隊列空出位置可以存入數據。阻塞隊列常用於生產者和消費者的場景,生產者是往隊列裏添加數據的線程,消費者是從隊列裏取出數據的線程。阻塞隊列就是生產者存放數據的容器,而消費者也只從容器裏取數據
線程池實現解耦的關鍵就是有了 任務隊列/阻塞隊列 的存在。線程池中是以生產者消費者模式+阻塞隊列來實現的,任務隊列負責緩存外部提交的任務,線程負責從任務隊列取出任務,這樣客戶端提交的任務就避免了和線程直接關聯
選擇不同的阻塞隊列可以實現不一樣的任務存取策略:
7、任務的拒絕策略
隨着客戶端不斷地提交任務,當前線程池大小也會不斷增加。在當前線程池大小達到 corePoolSize 的時候,新提交的任務會被緩存到任務隊列之中,由線程後續不斷從隊列中取出任務並執行。當任務隊列滿了之後,線程池就會創建非核心線程。當線程總數達到 maximumPoolSize 且所有線程都處於工作狀態,同時任務隊列也滿了後,客戶端再次提交任務時就會被拒絕。而被拒絕的任務具體的處理策略則由 RejectedExecutionHandler
來進行定義
public interface RejectedExecutionHandler {
void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
}
當客戶端提交的任務被拒絕時,線程池關聯的 RejectedExecutionHandler 對象的 rejectedExecution
方法就會被調用,相應的拒絕策略可以由客戶端來指定
ThreadPoolExecutor 提供了以下幾種拒絕策略,默認使用的是 AbortPolicy
實現類 | 策略 |
---|---|
AbortPolicy | 直接拋出異常,是 ThreadPoolExecutor 的默認策略 |
DiscardPolicy | 直接丟棄該任務,不做任何響應也不會拋出異常 |
DiscardOldestPolicy | 如果線程池未被停止,則將工作隊列中最老的任務丟棄,然後嘗試接納該任務 |
CallerRunsPolicy | 如果線程池未被停止,則直接在客戶端線程上執行該任務 |
任務的拒絕策略只會在提交任務的時候被觸發,即只在 execute(Runnable command)
方法中被觸發到。execute(Runnable command)
方法會判斷當前狀態是否允許接受該任務,如果不允許的話則會走拒絕任務的流程
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
if (isRunning(c) && workQueue.offer(command)) { //線程池還處於運行狀態且成功添加任務到任務隊列
//需要重新檢查下運行狀態
//因爲等執行到這裏時,線程池可能被其它線程關閉了
int recheck = ctl.get();
//1、如果線程池已經處於非運行狀態了
//1.1、如果移除 command 成功,則走拒絕策略
//1.2、如果移除 command 失敗(因爲 command 可能已經被其它線程拿去執行了),則走第 3 步
//2、如果線程池還處於運行狀態,則走第 3 步
//3、如果當前線程數量爲 0,則創建線程進行處理
//第 3 步的意義在於:corePoolSize 可以被設爲 0,所以這裏需要檢查下,在需要的時候創建一個非核心線程
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
//如果線程池處於非運行狀態了,或者是處於運行狀態但隊列已滿了,此時就會走到這裏
//在這裏嘗試創建一個非核心線程
//如果線程創建失敗,說明要麼是線程池當前狀態大於等於 STOP,或者是任務隊列已滿且線程總數達到 maximumPoolSize 了
//此時就走拒絕策略
else if (!addWorker(command, false))
reject(command);
}
final void reject(Runnable command) {
handler.rejectedExecution(command, this);
}
8、監控線程池的運行狀態
ThreadPoolExecutor 提供了多個配置參數以便滿足多種不同的需求,這些配置參數包含:corePoolSize、maximumPoolSize、keepAliveTime、allowCoreThreadTimeOut 等。但很多時候我們一開始使用線程池時並不知道該如何配置參數才最爲適應當前需求,那麼就只能通過監控線程池的運行狀態來進行考察,最終得到一份最合理的配置參數
可以通過 ThreadPoolExecutor 的以下幾個屬性來監控線程池的運行狀態:
- taskCount:線程池已執行結束(不管成功與否)的任務數加上任務隊列中目前包含的任務數
- completedTaskCount:線程池已執行結束(不管成功與否)的任務數,小於等於 taskCount
- largestPoolSize:線程池曾經創建過的最大線程數量。如果該數值等於 maximumPoolSize 那就說明線程池曾經滿過
- getPoolSize():獲取當前線程總數
- getActiveCount():獲取當前正在執行任務的線程總數
此外,ThreadPoolExecutor 也預留了幾個鉤子方法可以由子類去實現。通過以下幾個方法,就可以實現每個任務開始執行前和執行後,以及線程池走向終止時插入一些自定義的監控代碼,以此來實現:計算任務的平均執行時間、最小執行時間和最大執行時間等功能
protected void beforeExecute(Thread t, Runnable r) { }
protected void afterExecute(Runnable r, Throwable t) { }
protected void terminated() { }
四、線程池故障
1、線程池死鎖
多個線程會因爲循環等待對方持有的排他性資源而導致死鎖,線程池也可能會因爲多個任務間的相互依賴而導致線程池死鎖。例如,如果在線程池中執行的任務 A 在其執行過程中又向同個線程池提交了任務 B,且任務 A 的執行結束又依賴於任務 B 的執行結果,那麼就可能出現這樣的一種極端情形:線程池中的所有正在執行任務的線程都在等待其它任務的處理結果,而這些任務均在任務隊列中處於待執行狀態,且由於線程總數已經達到線程池的最大線程數量限制,所以任務隊列中的任務就會一直無法被執行,最終導致所有任務都無法完成,從而形成線程池死鎖
因此,提交給同一個線程池的任務必須是沒有互相依賴關係的。對於有依賴關係的任務,應該提交給不同的線程池,以此來避免死鎖的發生
2、線程泄漏
線程泄漏指由於某種原因導致線程池中實際可用的線程變少的一種異常情況。如果線程泄漏持續存在,那麼線程池中的線程會越來越少,最終使得線程池再也無法處理任務。導致線程泄露的原因可能有兩種:由於線程異常自動終止或者由於程序缺陷導致線程處於非有效運行狀態。前者通常是由於 Thread.run()
方法中沒有捕獲到任務拋出的 Exception 或者 Error 導致的,使得相應線程被提前終止而沒有相應更新線程池當前的線程數量,ThreadPoolExecutor 內部已經對這種情形進行了預防。後者可能是由於客戶端提交的任務包含阻塞操作(Object.wait() 等操作),而該操作又沒有相應的時間或者條件方面的限制,那麼就有可能導致線程一直處於等待狀態而無法執行其它任務,這樣最終也是形成了線程泄漏
五、總結
線程池通過複用一定數量的線程來執行不斷被提交的任務,除了可以節約線程這種有限而昂貴的資源外,還包含以下好處:
- 提高響應速度。ThreadPoolExecutor 提供了預先創建一定數量的線程的功能,使得後續提交的任務可以立即被執行而無須等待線程被創建,從而提高了系統的響應速度
- 抵消線程創建的開銷。一個線程可以先後用於執行多個任務,那創建線程帶來的成本(資源和時間)就可以看做是被平攤到其執行的所有任務中。一個線程執行的任務越多,那麼創建該線程的“性價比”就越高
- 封裝了任務的具體執行過程。線程池封裝了每個線程在創建、管理、複用、回收等各個階段的邏輯,使得客戶端代碼只需要提交任務和獲取任務的執行結果,而無須關心任務的具體執行過程。即使後續想要將任務的執行方式從併發改爲串行,往往也只需要修改線程池內部的處理邏輯即可,而無需修改客戶端代碼
- 減少銷燬線程的開銷。JVM 在銷燬一個已經停止的線程時也有着資源和時間方面的開銷,採用線程池可以避免頻繁地創建線程,從而減少了銷燬線程的次數,減少了相應開銷