理論知識
週末上海下起了雨也降溫了,無事打開電腦看看源碼,就想到了線程池。線程池的技術網絡上已經有很多文章都已經寫過了,而且理論都是一樣的。
但是理論歸理論,面試的時候也許你剛好看了一篇能應付過去,但是如果深究細節可能就會懵逼。所以我很建議任何理論我們都需要自己去探究一下才好,自己實踐過的纔有自己的理解而不是死記硬背,這樣纔會經久不忘。
線程池屬於開發中常見的一種池化技術,這類的池化技術的目的都是爲了提高資源的利用率和提高效率,類似的HttpClient連接池,數據庫連接池等。
在沒有線程池的時候,我們要創建多線程的併發,一般都是通過繼承 Thread 類或實現 Runnable 接口或者實現 Callable 接口,我們知道線程資源是很寶貴的,而且線程之間切換執行時需要記住上下文信息,所以過多的創建線程去執行任務會造成資源的浪費而且對CPU影響較大。
爲了方便, JDK 1.5 之後爲我們提供了幾種創建線程池的方法:
- Executors.newFixedThreadPool(nThreads):創建一個定長線程池,可控制線程最大併發數,超出的線程會在隊列中等待。
- Executors.newCachedThreadPool():創建一個可緩存線程池,如果線程池長度超過處理需要,可靈活回收空閒線程,若無可回收,則新建線程。
- Executors.newSingleThreadExecutor():創建一個單線程化的線程池,它只會用唯一的工作線程來執行任務, 保證所有任務按照指定順序(FIFO, LIFO, 優先級)執行。
- Executors.newScheduledThreadPool(nThreads):創建一個定長線程池,支持定時及週期性任務執行。
雖然這些都是 JDK 默認提供的,但是還是要說它們的定製性太差了而且有點雞肋,很多時候不能滿足我們的需求。例如通過 newFixedThreadPool 方式創建的固定線程池,它內部使用的隊列是 LinkedBlockingQueue,但是它的隊列大小默認是 Integer.MAX_VALUE,這會有什麼問題?
當核心線程滿了的時候,任務會進入隊列中等待,直到隊列滿了爲止。但是也許任務還未達到 Integer.MAX_VALUE
這個值的時候,內存就已經 OOM 了,因爲內存放不下這麼多的任務,畢竟內存大小有限。
所以更多的時候我們都是自定義線程池,也就是使用 new ThreadPoolExecutor 的方式,其實你看源碼你可以發現以上的4個線程池技術底層都是通過 ThreadPoolExecutor 來創建的,只不過它們自己爲我們填充了這些參數的固定值而已。
ThreadPoolExecutor 的構造函數如下所示:
ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler);
我們來看下這幾個核心參數的涵義和作用:
- corePoolSize: 爲線程池的核心線程基本大小。
- maximumPoolSize: 爲線程池最大線程大小。
- keepAliveTime 和 unit 則是線程空閒後的存活時間。
- workQueue: 用於存放任務的阻塞隊列。
- handler: 當隊列和最大線程池都滿了之後的飽和策略。
通過這些參數的配置使得整個線程池的工作流程如下:
前幾年一般普通的技術面試瞭解了以上的知識內容也差不多就夠了,但是目前的大環境的影響或者面試更高級的開發上面的知識點是經不起深度考問的。例如以下幾個問題你是否瞭解:線程池的內部有哪些狀態?是如何判斷核心線程數是否已滿的?最大線程數是否包含核心線程數?當線程池中的線程數剛好達到 maximumPoolSize 這個值的時候,這個任務能否正常被執行?......,想要了解這些問題的答案我們只能在線程池的源碼中尋找了。
實戰模擬測試
我們自定義一個線程池,然後通過 for 循環連續創建10個任務並打印線程執行信息,整體代碼如下所示:
public static void main(String[] args) {
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(3, 6, 5L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(4));
for (int i = 0; i < 10; i++) {
threadPoolExecutor.execute(() -> {
System.out.println("測試線程池:" + Thread.currentThread().getName() + "," + threadPoolExecutor.toString());
});
}
}
當 corePoolSize = 3,maximumPoolSize = 6,workQueue 大小爲4的時候,我們的打印信息爲:
可以發現總的創建了6個線程來執行完成了10個任務,其實很好理解,c=3個核心線程執行了3個任務,然後4個任務在隊列中等待覈心線程執行,最後額外創建了e=3個線程執行了剩下的3個任務,總創建的線程數就是 c + e = 6 <= 6(最大線程數)。
如果我們調整對象創建的時候的構造函數參數,例如
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(3, 5, 5L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(2));
我們再次執行上述的代碼,則會報錯,拋出如下 RejectedExecutionException 異常信息,可以看到是因爲拒絕策略攔截的異常信息。
還是按照上面的邏輯分析,這時核心線程數是 c = 3,而阻塞隊列的大小是 2,因此核心線程會處理掉其中5個任務,而剩下的5個任務會額外創建 e=5個線程去執行,那麼總線程數就是 c + e = 8,但是這時的最大線程數 maximumPoolSize = 5,因此超過了最大線程數的限制,這時就執行了默認的拒絕策略拋出異常。其實它在準備創建第6個線程的時候就已經報錯了,從這裏也可以得知只要創建的總線程數 >= maximumPoolSize 的時候,線程池就不會繼續執行任務了而會去執行拒絕策略的邏輯。
技術來源於生活
人們常常在生活中遇到一些困難的時候會進行頭腦風暴從而產生一些意想不到的解決方案,這些都是思想和智慧的結晶。我們很多技術的解決方案也都來源於生活。
我經常想如果以後不做程序員應該做什麼?餐飲似乎是最大衆的了,畢竟民以食爲天。
開餐館前期肯定不能做太大,一是本金的問題,還有就是需要市場試水。在市場需求不明確的情況下租個小店面還是靠譜的,就算虧也不會太多。
店面租個幾十平的,就做香辣烤魚,餐桌大概15桌的樣子。然後就是員工了,除了廚師主要是服務員了,但是我不能招15個服務員啊,每桌分配一個太浪費了,需要提高資源利用率控制成本,所以員工不能招太多,我只需要招5個固定服務員負責在大廳招呼顧客和傳菜就可以了,每個人負責3個餐桌。
但是我沒想到我們餐館做的烤魚很合大衆口味,很受歡迎又加上營銷效果好,成了一家網紅餐館。生意更是蒸蒸日上,每天座無虛席。但是空間有限啊,所以我們只能讓後來無座的顧客稍微等候了,於是我們安排了一個取號排隊等候區,顧客等待叫號有序就餐。
這時候餐館的人員不變,仍然是5個服務員負責處理大廳的主要服務工作,同時排隊等候區面積也不能過大,有個範圍限制,不能影響我們的正常人員活動,同時也不能超過餐館的範圍排到餐館外,如果顧客排隊站到門外馬路上了,這是就很危險的。隨着口碑的發酵,一傳十,十傳百,我們的顧客絡繹不絕,同時我們爲了提高消費率又做起了外賣的服務,可以打包外帶。
爲了避免發生上述這種危險的情況和提高訂單處理率,我們只能額外請一些臨時工了,讓他們來幫忙處理我們的外賣訂單從而提高業務處理能力。
但是也不是請的越多越好,我們有成本控制,因爲請的臨時工我們也需要付工資。那怎麼辦呢?最終只能忍痛了啊,對於超出我們處理能力的訂單,我們就採取一定的拒絕策略,例如告知顧客當天的份額已經售罄,請改天再來。
以上就是我們線程池運行的一個現實生活中的例子,核心線程就是我們的5個固定服務員,而排隊等候區就是我們的等待隊列,隊列不能設爲無限大,因爲會造成OOM,如果隊列滿了線程池會另起額外線程去處理任務,也就是上述例子中的臨時工,餐館有經營成本控制所以有員工上限,不能請過多的臨時工,這就是最大線程數。如果臨時工達到最大數且隊列也滿了,那麼我們只能通過拒絕策略暫時不接受額外的服務要求了。
一起看源碼
口說無憑,理論都是這樣說的,那實際上源碼是不是真是這樣寫的呢?我們一起來看下線程池的源碼。通過 threadPoolExecutor.execute(...)
的入口進入源碼,刪除了註釋信息之後的源碼內容如下,由於封裝的好,所以只有短短几行。
public void execute(Runnable command) {
// #1 任務非空校驗
if (command == null)
throw new NullPointerException();
// #2 添加核心線程執行任務
int c = ctl.get();
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
// #3 任務入隊列
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);
}
// #4 添加普通線程執行任務,如果失敗則執行拒絕策略
else if (!addWorker(command, false))
reject(command);
}
如果不關注細節只關注整體,從以上源碼中我們可以發現其中主要分爲了四個步驟來處理邏輯。排除第一步的非空校驗代碼,我們可以看出剩下的三步其實就是我們線程池的運行邏輯,也就是上面的運行流程圖的邏輯內容。
- (1) 任務的非空校驗。
- (2) 獲取當前RUNNING的線程數,如果小於核心線程數,則創建核心線程去執行任務,否則走#3。
- (3) 如果當前線程池處於RUNNING狀態,那麼就將任務放入隊列中。這時還會再做個雙重校驗,因爲可能存在有些線程在我們上次檢查後死了,或者從我們進入這個方法後pool被關閉了,所以我們需要再次檢查state。如果線程池停止了就需要回滾剛纔的添加任務到隊列中的操作並通過拒絕策略拒絕該任務,或者如果池中沒有線程了,則新開啓一個線程執行任務。
- (4) 如果隊列滿了之後無法在將任務加入隊列,則創建新的線程去執行任務,如果也失敗了,那麼就可能是線程池關閉了或者線程池飽和了,這時執行拒絕策略不再接受任務。
雙重校驗中有以下兩個點需要注意:
1. 爲什麼需要 double check 線程池的狀態?
在多線程環境下,線程池的狀態時刻在變化,而 ctl.get() 是非原子操作,很有可能剛獲取了線程池狀態後線程池狀態就改變了。判斷是否將 command 加入 workque 是線程池之前的狀態。倘若沒有 double check,萬一線程池處於非 running 狀態(在多線程環境下很有可能發生),那麼 command 永遠不會執行。2、爲什麼 addWorker(null, false) 的任務爲null?
addWorker(null, false),這個方法執行時只是創建了一個新的線程,但是沒有傳入任務,這是因爲前面已經將任務添加到隊列中了,這樣可以防止線程池處於 running 狀態,但是沒有線程去處理這個任務。
而根據以上代碼的具體步驟我們可以畫出詳細的執行流程,如下圖所示
以上的源碼其實只有10幾行,看起來很簡單,主要是它的封裝性比較好,其中主要有兩個點需要重點解釋,分別是:線程池的狀態和 addWorker()
添加工作的方法,這兩個點弄明白了這段線程池的源碼差不多也就理解了。
線程池運行狀態-runState
線程有狀態,線程池也有它的運行狀態,這些狀態提供了主生命週期控制,伴隨着線程池的運行,由內部來維護,從源碼中我們可以發現線程池共有5個狀態:RUNNING
,SHUTDOWN
,STOP
,TIDYING
,TERMINATED
。
各狀態值所代表的的含義和該狀態值下可執行的操作,具體信息如下:
運行狀態 | 狀態描述 |
---|---|
RUNNING | 接收新任務,並且也能處理阻塞隊列中的任務。 |
SHUTDOWN | 不接收新任務,但是卻可以繼續處理阻塞隊列中的任務。 |
STOP | 不接收新任務,同時也不處理隊列任務,並且中斷正在進行的任務。 |
TIDYING | 所有任務都已終止,workercount(有效線程數)爲0,線程轉向 TIDYING 狀態將會運行 terminated() 鉤子方法。 |
TERMINATED | terminated() 方法調用完成後變成此狀態。 |
生命週期狀態流轉如下圖所示:
很多時候我們表示狀態都是通過簡單的 int 值來表示,例如數據庫數據的刪除標誌 delete_flag 其中0表示有效,1表示刪除。而在線程池的源碼裏我們可以看到它是通過如下方式來進行表示的,
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;
// 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;
線程池內部使用一個變量維護兩個值:運行狀態(runState)和線程數量 (workerCount)何做到的呢?將十進制 int 值轉換爲二進制的值,共32位,其中高3位代表運行狀態(runState ),而低29位代表工作線程數(workerCount)。
關於內部封裝的獲取生命週期狀態、獲取線程池線程數量的計算方法如以下代碼所示:
//獲取線程池狀態
private static int runStateOf(int c) { return c & ~CAPACITY; }
//獲取線程數量
private static int workerCountOf(int c) { return c & CAPACITY; }
// Packing and unpacking ctl
private static int ctlOf(int rs, int wc) { return rs | wc; }
通過巧妙的位運算可以分別獲取高3位的運行狀態值和低29位的線程數量值,如果感興趣的可以去看下具體的實現代碼,這裏就不再贅述了。
添加工作線程-addWorker
添加線程是通過 addWorker() 方法來實現的,這個方法有兩個入參,Runnable firstTask
和 boolean core
。
private boolean addWorker(Runnable firstTask, boolean core){...}
- Runnable firstTask 即是當前添加的線程需要執行的首個任務.
- boolean core 用來標記當前執行的線程是否是核心線程還是普通線程.
返回前面的線程池的 execute() 方法的代碼中,可以發現這個addWorker() 有三個地方在調用,分別在 #2,#3和#4。
- #2:當工作線程數 < 核心線程數的時候,通過
addWorker(command, true)
添加核心線程執行command任務。 - #3:double check的時候,如果發現線程池處於正常運行狀態但是裏面沒有工作線程,則添加個空任務和一個普通線程,這樣一個 task 爲空的 worker 在線程執行的時候會去阻塞任務隊列裏拿任務,這樣就相當於創建了一個新的線程,只是沒有馬上分配任務。
- #4:隊列已滿的情況下,通過添加普通線程(非核心線程)去執行當前任務,如果失敗了則執行拒絕策略。
addWorker() 方法調用的地方我們看完了,接下來我們一起來看下它裏面究竟做了些什麼,源碼如下:
private boolean addWorker(Runnable firstTask, boolean core) {
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()))
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()) // 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;
}
這個方法稍微有點長,我們分段來看下,將上面的代碼我們拆分成兩個部分來看,首先看第一部分:
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()))
return false;
for (;;) {
int wc = workerCountOf(c);
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maximumPoolSize))
return false;
// 嘗試通過CAS方式增加workerCount
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
}
}
這部分代碼有兩層嵌套的 for 死循環,在第一行有個retry:
代碼,這個也許有些同學沒怎麼見過,這個是相當於是一個位置標記,retry後面跟循環,標記這個循環的位置。
我們平時寫 for 循環的時候,是通過continue;
或break;
來跳出當前循環,但是如果我們有多重嵌套的 for 循環,如果我們想在裏層的某個循環體中當達到某個條件的時候直接跳出所有循環或跳出到某個指定的位置,則使用retry:
來標記這個位置就可以了。
代碼中共有4個位置有改變循環體繼續執行下去,分別是兩個return false;
,一個break retry;
和一個continue retry;
。
首先我們來看下第一個return false;
,這個 return 在最外層的一個 for 循環,
if (rs >= SHUTDOWN && !(rs == SHUTDOWN && firstTask == null && ! workQueue.isEmpty()))
return false;
這是一個判斷線程池狀態和線程隊列情況的代碼,這個邏輯判斷有點繞可以改成
rs >= shutdown && (rs != shutdown || firstTask != null || workQueue.isEmpty())
這樣就好理解了,邏輯判斷成立可以分爲以下幾種情況直接返回 false,表示添加工作線程失敗。
- rs > shutdown:線程池狀態處於
STOP
,TIDYING
,TERMINATED
時,添加工作線程失敗,不接受新任務。 - rs >= shutdown && firstTask != null:線程池狀態處於
SHUTDOWN
,STOP
,TIDYING
,TERMINATED
狀態且worker的首個任務不爲空時,添加工作線程失敗,不接受新任務。 - rs >= shutdown && workQueue.isEmppty:線程池狀態處於
SHUTDOWN
,STOP
,TIDYING
,TERMINATED
狀態且阻塞隊列爲空時,添加工作線程失敗,不接受新任務。
這樣看來,最外層的 for 循環是不斷的校驗當前的線程池狀態是否能接受新任務,如果校驗通過了之後才能繼續往下運行。
然後接下來看第二個return false;
,這個 return 是在內層的第二個 for 循環中,是判斷線程池中當前的工作線程數量的,不滿足條件的話直接返回 false,表示添加工作線程失敗。
- 工作線程數量是否超過可表示的最大容量(CAPACITY).
- 如果添加核心工作線程,是否超過最大核心線程容量(corePoolSize).
- 如果添加普通工作線程,是否超過線程池最大線程容量(maximumPoolSize).
後面的break retry;
,表示如果嘗試通過CAS方式增加工作線程數workerCount成功,則跳出這個雙循環,往下執行後面第二部分的代碼,而continue retry;
是再次校驗下線程池狀態是否發生變化,如果發生了變化則重新從最外層 for 開始繼續循環執行。
通過第一部分代碼的解析,我們發現只有break retry;
的時候才能執行到後面第二部分的代碼,而後面第二部分代碼做了些什麼呢?
boolean workerStarted = false;
boolean workerAdded = false;
Worker w = null;
try {
//創建Worker對象實例
w = new Worker(firstTask);
//獲取Worker對象裏的線程
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());
//滿足 rs < SHUTDOWN 判斷線程池是否是RUNNING,或者
//rs == SHUTDOWN && firstTask == null 線程池如果是SHUTDOWN,
//且首個任務firstTask爲空,
if (rs < SHUTDOWN ||
(rs == SHUTDOWN && firstTask == null)) {
if (t.isAlive()) // precheck that t is startable
throw new IllegalThreadStateException();
//將Worker實例加入線程池workers
workers.add(w);
int s = workers.size();
if (s > largestPoolSize)
largestPoolSize = s;
//線程添加成功標誌位 -> true
workerAdded = true;
}
} finally {
//釋放鎖
mainLock.unlock();
}
//如果worker實例加入線程池成功,則啓動線程,同時修改線程啓動成功標誌位 -> true
if (workerAdded) {
t.start();
workerStarted = true;
}
}
} finally {
if (! workerStarted)
//添加線程失敗
addWorkerFailed(w);
}
return workerStarted;
這部分代碼主要的目的其實就是啓動一個線程,前面是一堆的條件判斷,看是否能夠啓動一個工作線程。它由兩個try...catch...finally
內容組成,可以將他們拆開來看,這樣就很容易看懂。
我們先看裏面一層的try...catch...finally
,當Worker實例中的 Thread 線程不爲空的時候,開啓一個獨佔鎖ReentrantLock mainLock
,防止其他線程也來修改操作。
try {
//獲取線程池運行狀態
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();
}
- 首先檢查線程池的狀態,當線程池處於 RUNNING 狀態或者線程池處於 SHUTDOWN 狀態但是當前線程的 firstTask 爲空,滿足以上條件時才能將 worker 實例添加進線程池,即
workers.add(w);
。 - 同時修改 largestPoolSize,largestPoolSize變量用於記錄出現過的最大線程數。
- 將標誌位 workerAdded 設置爲 true,表示添加工作線程成功。
- 無論成功與否,在 finally 中都必須執行
mainLock.unlock()
來釋放鎖。
外面一層的try...catch...finally
主要是爲了判斷工作線程是否啓動成功,如果內層try...catch...finally
代碼執行成功,即 worker 添加進線程池成功,workerAdded 標誌位置爲true,則啓動 worker 中的線程 t.start()
,同時將標誌位 workerStarted 置爲 true,表示線程啓動成功。
if (workerAdded) {
t.start();
workerStarted = true;
}
如果失敗了,即 workerStarted == false,則在 finally 裏面必須執行addWorkerFailed(w)
方法,這個方法相當於是用來回滾操作的,前面增的這裏移除,前面加的這裏減去。
private void addWorkerFailed(Worker w) {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
if (w != null)
//從線程池中移除worker實例
workers.remove(w);
//通過CAS,將工作線程數量workerCount減1
decrementWorkerCount();
//
tryTerminate();
} finally {
mainLock.unlock();
}
}
Worker類
上面我們分析了addWorker 方法的源碼,並且看到了 Thread t = w.thread
,workers.add(w)
和t.start()
等代碼,知道了線程池的運行狀態和添加工作線程的流程,那麼我們還有一些疑問:
- 這裏的 Worker 是什麼?和 Thread 有什麼區別?
- 線程啓動後是如何拿任務?在哪拿任務去執行的?
- 阻塞隊列滿後,額外新創建的線程是去隊列裏拿任務的嗎?如果不是那它是去哪拿的?
- 核心線程會一直存在於線程池中嗎?額外創建的普通線程執行完任務後會銷燬嗎?
Worker 是 ThreadPoolExecutor
的一個內部類,主要是用來維護線程執行任務的中斷控制狀態,它實現了Runnable 接口同時繼承了AQS,實現 Runnable 接口意味着 Worker 就是一個線程,繼承 AQS 是爲了實現獨佔鎖這個功能。
private final class Worker
extends AbstractQueuedSynchronizer
implements Runnable
{
/** Thread this worker is running in. Null if factory fails. */
final Thread thread;
/** Initial task to run. Possibly null. */
Runnable firstTask;
/** Per-thread task counter */
volatile long completedTasks;
//構造函數,初始化AQS的state值爲-1
Worker(Runnable firstTask) {
setState(-1); // inhibit interrupts until runWorker
this.firstTask = firstTask;
this.thread = getThreadFactory().newThread(this);
}
}
至於爲什麼沒有使用可重入鎖 ReentrantLock,而是使用AQS,爲的就是實現不可重入的特性去反應線程現在的執行狀態。
- lock方法一旦獲取了獨佔鎖,表示當前線程正在執行任務中。
- 如果正在執行任務,則不應該中斷線程。
- 如果該線程現在不是獨佔鎖的狀態,也就是空閒的狀態,說明它沒有在處理任務,這時可以對該線程進行中斷。
- 線程池在執行 shutdown 方法或 tryTerminate 方法時會調用 interruptIdleWorkers 方法來中斷空閒的線程,interruptIdleWorkers 方法會使用 tryLock 方法來判斷線程池中的線程是否是空閒狀態;如果線程是空閒狀態則可以安全回收。
Worker 類有一個構造方法,構造參數爲給定的首個任務 firstTask,並持有一個線程thread。thread是在調用構造方法時通過 ThreadFactory 來創建的線程,可以用來執行任務;
firstTask用它來初始化時傳入的第一個任務,這個任務可以有也可以爲null。如果這個值是非空的,那麼線程就會在啓動初期立即執行這個任務;如果這個值是null,那麼就需要創建一個線程去執行任務列表(workQueue)中的任務,也就是非核心線程的創建。
任務運行-runWorker
上面我們一起看過線程的啓動t.start()
,具體運行是在 Worker 的 run() 方法中
public void run() {
runWorker(this);
}
run() 方法中又調用了runWorker() 方法,所有的實現都在這裏
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) {
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
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);
}
}
很多人看到這樣的代碼就感覺頭痛,其實你細看,這裏面我們可以看關鍵點,裏面有三塊try...catch...finally
代碼,我們將這三塊分別單獨拎出來看並且將拋異常的地方暫時刪掉或註釋掉,這樣它看起來就清爽了很多
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;
w.firstTask = null;
//由於Worker初始化時AQS中state設置爲-1,這裏要先做一次解鎖把state更新爲0,允許線程中斷
w.unlock(); // allow interrupts
boolean completedAbruptly = true;
try {
// 循環的判斷任務(firstTask或從隊列中獲取的task)是否爲空
while (task != null || (task = getTask()) != null) {
// Worker加鎖,本質是AQS獲取資源並且嘗試CAS更新state由0更變爲1
w.lock();
// 如果線程池運行狀態是stopping, 確保線程是中斷狀態;
// 如果不是stopping, 確保線程是非中斷狀態.
if ((runStateAtLeast(ctl.get(), STOP) ||
(Thread.interrupted() &&
runStateAtLeast(ctl.get(), STOP))) &&
!wt.isInterrupted())
wt.interrupt();
//此處省略了第二個try...catch...finally
}
// 走到這裏說明某一次getTask()返回爲null,線程正常退出
completedAbruptly = false;
} finally {
//處理線程退出
processWorkerExit(w, completedAbruptly);
}
第二個try...catch...finally
try {
beforeExecute(wt, task);
Throwable thrown = null;
//此處省略了第三個try...catch...finally
} finally {
task = null;
w.completedTasks++;
w.unlock();
}
第三個try...catch...finally
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);
}
上面的代碼中可以看到有beforeExecute
、afterExecute
和terminaerd
三個函數,它們都是鉤子函數,可以分別在子類中重寫它們用來擴展ThreadPoolExecutor,例如添加日誌、計時、監視或者統計信息收集的功能。
- beforeExecute():線程執行之前調用
- afterExecute():線程執行之後調用
- terminaerd():線程池退出時候調用
這樣拆分完之後發現,其實主要注意兩個點就行了,分別是getTask()
和task.run()
,task.run()
就是運行任務,那我們繼續來看下getTask()
是如何獲取任務的。
獲取任務-getTask
private Runnable getTask() {
boolean timedOut = false; // Did the last poll() time out?
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);
//1.線程池狀態是STOP,TIDYING,TERMINATED
//2.線程池shutdown並且隊列是空的.
//滿足以上兩個條件之一則工作線程數wc減去1,然後直接返回null
if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
decrementWorkerCount();
return null;
}
int wc = workerCountOf(c);
//允許核心工作線程對象銷燬淘汰或者工作線程數 > 最大核心線程數corePoolSize
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
//1.工作線程數 > 最大線程數maximumPoolSize 或者timed == true && timedOut == true
//2.工作線程數 > 1 或者隊列爲空
//同時滿足以上兩個條件則通過CAS把線程數減去1,同時返回null。CAS把線程數減去1失敗會進入下一輪循環做重試
if ((wc > maximumPoolSize || (timed && timedOut))
&& (wc > 1 || workQueue.isEmpty())) {
if (compareAndDecrementWorkerCount(c))
return null;
continue;
}
try {
/// 如果timed爲true,通過poll()方法做超時拉取,keepAliveTime時間內沒有等待到有效的任務,則返回null
// 如果timed爲false,通過take()做阻塞拉取,會阻塞到有下一個有效的任務時候再返回(一般不會是null)
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();
if (r != null)
return r;
timedOut = true;
} catch (InterruptedException retry) {
timedOut = false;
}
}
}
裏面有個關鍵字allowCoreThreadTimeOut
,它的默認值爲false,在Java1.6開始你可以通過threadPoolExecutor.allowCoreThreadTimeOut(true)
方式來設置爲true,通過字面意思就可以明白這個字段的作用是什麼了,即是否允許核心線程超時銷燬。
默認的情況下核心線程數量會一直保持,即使這些線程是空閒的它也是會一直存在的,而當設置爲 true 時,線程池中 corePoolSize 線程空閒時間達到 keepAliveTime 也將銷燬關閉。
結尾
通過整片分析下來,線程池裏面有很多細節處需要注意,閱讀完源碼之後也理解了更多,解開了很多困惑,獲取到了更多的知識點,所以源碼的閱讀是很重要的。