概述
之前我寫了一篇博客,系統的介紹了線程池相關知識。感興趣的讀者可以點擊這裏查看之前的博客。
最近我打算實現一個簡單的線程池,在實現過程遇到很多問題,從中發現很多之前沒有搞懂的知識點。爲了查缺補漏,我打算整理一遍線程池核心功能的完整源碼。
線程池如何執行任務
本篇主要介紹線程池執行一個任務的全過程。爲了便於理解,我打算按照源碼執行順序展開,其中每個方法儘可能通過流程圖的方式展示。
在閱讀本篇博客前,需要你對線程池有一個大概的瞭解,本篇不會具體解釋某個屬性的作用,如果對線程池本身還不太熟悉的話,建議先點擊概述中的鏈接初步瞭解一下。
1、execute(Runnable command)
進入正題,線程池無論通過哪種方式執行任務,最終都會調用 execute() 方法。下面我們直接給出 execute() 方法的源碼:
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();
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
else if (!addWorker(command, false))
reject(command);
}
關於該方法的邏輯,我們直接通過以下流程圖介紹:
首先就流程圖中,幾個我認爲比較迷惑的點先列出來並給出我的理解:
-
爲什麼在任務入隊後,還需要判斷線程池狀態是否停止,並嘗試刪除任務呢?
答:這裏實際上是做了一個 複檢 的操作,爲的是防止在任務入隊期間線程池停止。
-
爲什麼要分兩種線程?一種以被執行任務爲參數,一種以null爲參數?
答:這裏的任務參數表示 第一個執行的任務。也就是說,該線程啓動後有一個初始任務。如果任務入隊的話,就不需要創建線程默認執行,因爲它遲早會從隊列中取出並執行。
-
核心線程和非核心的區別有哪些?
答:我認爲兩者本身沒有區別,只是在創建線程過程中,核心線程和非核心線程進行不同的邏輯判斷,核心線程根據核心線程數判斷,非核心線程根據所有線程數判斷。
總結:就我個人理解,execute() 方法主要判斷 是否需要創建 Worker對象 。而在線程池中,Worker類也就是線程工作類,也就是說該方法主要判斷當前任務是否需要創建新線程來執行。
根據閱讀上述代碼,我們會發現,只有以下三種情況纔會創建新的線程:
- 線程池中工作線程數量小於 核心線程數量 時,創建 核心線程。
- 線程池中阻塞隊列不爲空,工作線程數量等於0時,創建 非核心線程。
- 創建核心線程失敗,任務入隊失敗時,創建 非核心線程。
而這三種情況對應的實際場景依次分別是:
- 核心線程數量不夠,補充新的核心線程執行當前任務
- 線程池中沒有線程,並且阻塞隊列中存在未執行的任務,將任務入隊,按照隊列順序依次執行
- 核心線程已滿,阻塞隊列已滿,創建非核心線程執行當前任務
當然上述只是線程執行的第一步,下面我們來閱讀具體創建工作線程的代碼:
2、addWorker(Runnable firstTask, boolean core)
線程池執行 execute() 方法後,最終都要通過 addWorker() 方法創建的線程執行。下面我們直接給出 addWork() 方法的源代碼:
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;
}
}
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 {
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;
}
關於該方法的邏輯,我們也通過流程圖的方式展開:
首先我們解釋流程圖中的 特殊情況 :線程池處於 ShutDown狀態,當前任務爲空,並且阻塞隊列不爲空時。這也就對應上述 execute() 方法的第二種情況,唯一區別是,execute() 方法執行時線程池還處於 Running 狀態,而這裏線程池已經處於ShutDown狀態。
通過這裏也可以反映出線程池策略:線程池停止後不會處理新方法,但緩存隊列中的方法還會執行。
關於上述流程圖,我也列舉出部分疑問我和理解:
-
上述代碼中,爲什麼要加解鎖?
答:加解鎖期間,主要是爲了處理部分線程不安全的變量。如:workers 採用 HashSet 數據結構,largestPoolSize 是 int 類型的,這兩個全局變量都是線程不安全的。
-
上述代碼中,創建線程後,爲什麼要判斷線程是否啓動?
答:關於這個問題我也不能理解:新創建的線程在沒有啓動時,isAlive() 方法一定會返回 false,也就是說該判斷一定不同通過。我懷疑該方法是爲了確定創建對象期間是否出錯,也就是初始化thread屬性期間沒有出現異常。
總結:addWorker() 方法主要完成 創建工作線程,並啓動該工作線程。有且僅當工作線程創建成功並啓動時返回 True。該方法在 execute() 方法的基礎上,增加了判斷線程數量的操作,其餘都是一些維護線程池屬性的操作。除此之外,它還實現了非Running狀態,拒絕執行任務的邏輯。
execute()方法 和 addWorker() 方法都是在主線程層面調用線程池執行任務,下面我們具體看一下工作線程是如何執行任務,也就是Worker類的源碼。
3、Worker 類
Worker類是線程池中用來封裝工作線程的核心類,我們直接看它的源碼:
private final class Worker
extends AbstractQueuedSynchronizer
implements Runnable
{
private static final long serialVersionUID = 6138294804551838833L;
final Thread thread;
Runnable firstTask;
volatile long completedTasks;
Worker(Runnable firstTask) {
setState(-1);
this.firstTask = firstTask;
this.thread = getThreadFactory().newThread(this);
}
public void run() {
runWorker(this);
}
// 略
}
首先該類繼承 AbstractQueuedSynchronizer 類,簡化部分同步操作。其實也很容易猜到,這裏簡化的是每個任務執行所獲取和釋放的鎖定。
其次該類實現了 Runnable 接口,也就是說可以將該類對象作爲參數創建線程,啓動線程來執行該類的run() 方法。
最後該類有兩個重要的屬性:thread 和 firstTask。thread 表示工作線程本身,firstTask 表示該工作線程的首任務。在構造方法中,通過Worker對象本身作爲參數創建線程 thread,而該 thread 又是 Worker 對象的屬性。我們在啓動該Worker對象的thread屬性時,實際上就是執行Worker對象的run()方法,也就是線程體。
關於這樣做的好處我是這樣理解的:
- 線程池可以從外部維護 Worker對象,而不是 Thread 對象,易於管理
- 可以通過Worker對象實現部分方法,通過調用這些方法操作線程,而不是通過thread對象
- 可以在Worker對象中維護屬性記錄線程的狀態,不用每次調用Thread方法判斷
下面我們來看 runWorker() 方法的源碼,熟悉線程體都做了什麼
4、runWorker(Worker w)
所有工作線程最終都會走到 runWorker() 方法來執行任務,這裏我直接貼出源碼:
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();
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);
}
}
上述代碼我們依舊通過流程圖的形式展開,如下所示(流程圖中部分非關鍵邏輯可能存在錯誤,例如線程調用interrupt()方法並不會立即停止):
這個方法的核心功能就是告訴我們,工作線程通過直接執行Runnable對象方法的run方法執行任務。
其中 執行前期操作 和 執行後期操作 都可能產生異常,方法體內可以做異常拋出等操作。
需要注意的一點是:有且僅當getTask()方法拋出異常時,纔會導致停止標誌以true的形式執行 processWorkerExit() 方法。關於該方法的邏輯本篇暫時不做結束,後續再其他博客中展開。
通過該方法,我們可以看出線程池中,工作線程能夠複用的主要原理是:通過單線程執行多個任務的 run() 方法。其中在處理完某個任務後,通過 getTask() 方法獲取新的任務執行。
最後,我們來看一下 getTask() 方法的實現原理。
5、getTask()
在線程池中,工作線程循環調用 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);
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;
}
}
}
其中該方法邏輯有點繞,我先通過流程圖的方式給出大體邏輯,下面通過文字對比較繞的模塊單獨介紹:
判斷線程池是否停止:可以理解爲線程池是否不工作了,主要表示以下兩種場景:
- 線程池狀態爲STOP及以後
- 線程池狀態爲SHUTDOWN,並且阻塞隊列已經爲空
這裏主要爲了區分線程池處於SHUTDOWN,但是隊列不爲空的情況。我們知道,線程池處於SHUTDOWN狀態時,還是會處理阻塞隊列中的任務的,也就是還在工作的。
判斷是否超時回收:當存在以下兩種情況時,超時線程纔會被回收:
- 配置 allowCoreThreadTimeOut 爲 True,也就是所有線程空閒都會回收
- 線程數超過最大核心線程數,也就可以理解爲非核心線程空閒都會被回收
判斷線程是否可回收:判斷一個線程是否可回收主要集中在以下兩種情況,其中無論哪種情況至少得滿足下述兩種情況中的一種:
- 線程數大於1,也就是說有線程可回收
- 阻塞隊列爲空,也就是說當前沒有任務需要執行
判斷可回收的情況如下:
- 線程數大於最大線程數
- 線程已超時,並且該線程超時會被回收
流程圖中比較模糊的幾個判斷及解釋已經給出,現在我們再來看 getTask()方法。
我理解 getTask() 方法的核心作用是 控制工作線程的週期。可以總結爲:
- 如果當前線程是可回收的,超過最長空閒時返回null,線程執行 processWorkerExit() 方法被回收
- 如果線程是不可回收的,就阻塞線程,直到有任務時返回任務,交給工作線程處理
最後我們再來聊聊線程回收,通過總結上述代碼,我認爲只有三種情況纔會回收線程:
- 線程池不工作了
- 超時可回收線程超時了
- 線程數超過最大線程,並且當前沒有任務
**那麼線程池是如何做到阻塞工作線程不讓它被回收的呢?又是如何做到回收線程?
答:阻塞隊列的 take() 方法在隊列爲空時會阻塞,因此線程也會阻塞,此時線程就會一直等待任務,不被回收。阻塞隊列的 poll() 方法不會阻塞,如果線程池爲空,直接返回null。回到 runWorker() 方法,如果任務爲空,就會跳出循環執行 processWorkerExit() 方法,該方法會通過 tryTerminate() 方法停止線程並回收。