JDK ThreadPoolExecutor核心原理與實踐

語言: CN / TW / HK

一、內容概括

本文內容主要圍繞JDK中的ThreadPoolExecutor展開,首先描述了ThreadPoolExecutor的構造流程以及內部狀態管理的機理,隨後用大量篇幅深入原始碼探究了ThreadPoolExecutor執行緒分配、任務處理、拒絕策略、啟動停止等過程,其中對Worker內建類進行重點分析,內容不僅包含其工作原理,更對其設計思路進行了一定分析。文章內容既包含了原始碼流程分析,還具有設計思路探討和二次開發實踐。

圖片

二、構造ThreadPoolExecutor

2.1 執行緒池引數列表

大家可以通過如下構造方法建立執行緒池(其實還有其它構造器,大家可以深入原始碼進行檢視,但最終都是呼叫下面的構造器建立執行緒池);

java public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) { ... }

其中的構造引數的作用如下:

  • corePoolSize:核心執行緒數。提交任務時,當執行緒池中的執行緒數 小於 corePoolSize 時,會 新 建立一個核心執行緒執行任務。當執行緒數 等於 corePoolSize 時,會將任務 新增進任務佇列。

  • maximumPoolSize:最大執行緒數。提交任務時,當 任務佇列已滿 並且執行緒池中的匯流排程數 不大於 maximumPoolSize 時,執行緒池會令非核心執行緒執行提交的任務。當 大於 maximumPoolSize 時,會執行拒絕策略。

  • keepAliveTime:非核心執行緒 空閒時 的存活時間。

  • unit:keepAliveTime 的單位。

  • workQueue:任務佇列(阻塞佇列)。

  • threadFactory:執行緒工廠。執行緒池用來新建立執行緒的工廠類。

  • handler:拒絕策略,執行緒池遇到無法處理的情況時會執行該拒絕策略選擇拋棄或忽略任務等。

2.2 執行流程概述

由構造引數的作用我們可知,執行緒池中由幾個重要的元件:核心執行緒池 、 空閒(非核心)執行緒池 和 阻塞佇列。這裡首先給出執行緒池的核心執行流程圖,大家首先對其有個印象,之後分析原始碼就會輕鬆一些了。

下面對流程圖中一些註釋說明下:cap表示池的容量,size表示池中正在執行的執行緒數。對於阻塞佇列來說,cap表示佇列容量,size表示已經入隊的任務數量。cpS<cpc表示執行中的核心執行緒數小於執行緒池設定核心執行緒數的情況。

圖片

1)當核心執行緒池 未 “滿” 時,會建立新的核心執行緒執行提交的任務。這裡的 “滿” 指的是核心執行緒池中的數量(size)小於容量(cap),此時會通過執行緒工廠新建立執行緒執行提交任務。

2)當核心執行緒池 已 “滿” 時,會將提交的任務push進任務佇列中,等待核心執行緒的釋放。一旦核心執行緒釋放後,將會從任務佇列中pull task繼續執行。因為使用的是阻塞佇列,對於已經釋放的核心執行緒,也會阻塞在獲取任務的過程中。

3)當任務佇列也滿了時(這裡的滿是指真的滿了,當然暫不考慮無界佇列情況),會從空閒執行緒池中繼續建立執行緒執行提交的任務。但空閒執行緒池中的執行緒是有存活時間(keepAliveTime)的,當執行緒執行完任務後,只能存活 keepAliveTime 時長,時間一過,執行緒就得被銷燬。

4)當空閒執行緒池的執行緒數不斷增加,直到ThreadPoolExecutor中的匯流排程數大於 maximumPoolSize 時,會拒絕執行任務,將提交的任務交給 RejectedExecutionHandler 進行後續處理。

上面所說的核心執行緒池和空閒執行緒池只是抽象出來的一個概念,後面我們將對其具體內容進行分析。

2.3 常用執行緒池

在進入 ThreadPoolExecutor 的原始碼分析前,我們先介紹下常用的執行緒池(其實並不常用,只是JDK自帶了)。這些執行緒池可由 Executors 這個工具類(或叫執行緒池工廠)來建立。

2.3.1 FixedThreadPool

固定執行緒數執行緒池的建立方式如下:其中核心執行緒數與最大執行緒數固定且相等,採用以連結串列為底層結構的無界阻塞佇列。

java public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(), threadFactory); }

特點

  • 核心執行緒數與最大執行緒數相等,因此不會建立空閒執行緒。keepAliveTime 設定與否無關緊要。

  • 採用無界佇列,任務會被無限新增,直至記憶體溢位(OOM)。

  • 由於無界佇列不可能被佔滿,任務在執行前不可能被拒絕(前提是執行緒池一直處於執行狀態)。

應用場景

  • 適用於執行緒數固定的場景

  • 適用負載比較重的伺服器

2.3.2 SingleThreadExecutor

單執行緒執行緒池的建立方式如下:其中核心執行緒數與最大執行緒數都為1,採用以連結串列為底層結構的無界阻塞佇列。

java public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) { return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(), threadFactory)); }

特點

  • 與 FixedThreadPool 類似,只是執行緒數為1而已。

應用場景

  • 適用單執行緒的場景。

  • 適用於對提交任務的處理有順序性要求的場景。

2.3.3 CachedThreadPool

緩衝執行緒池的建立方式如下:其中核心執行緒數為0,最大執行緒數為Integer.MAX_VALUE(可以理解為無窮大)。採用同步阻塞佇列。

java public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>(), threadFactory); }

特點

  • 核心執行緒數為0,則初始就建立空閒執行緒,並且空閒執行緒的只能等待任務60s,60s內沒有提交任務,空閒執行緒將被銷燬。

  • 最大執行緒數為無窮大,這樣會造成巨量執行緒同時執行,CPU負載過高,導致應用崩潰。

  • 採用同步阻塞佇列,即佇列不儲存任務。提交一個消費一個。由於最大執行緒數為無窮大,因此,只要提交任務就一定會被消費(應用未崩潰前)。

應用場景

  • 適用於耗時短、非同步的小程式。

  • 適用於負載較輕的伺服器。

三、執行緒池狀態以及活躍執行緒數

ThreadPoolExecutor 中有兩個非常重要的引數:執行緒池狀態 (rs) 以及 活躍執行緒數(wc)。前者用於標識當前執行緒池的狀態,並根據狀態量來控制執行緒池應該做什麼;後者用於標識活躍執行緒數,根據數量控制應該在核心執行緒池還是空閒執行緒池建立執行緒。

ThreadPoolExecutor 用一個 Integer 變數(ctl)來設定這兩個引數。我們知道,在不同作業系統下,Java 中的 Integer 變數都是32位,ThreadPoolExecutor 使用前3位(31~29)表示執行緒池狀態,用後29位(28~0)表示活躍執行緒數。

圖片

這樣設定的目的是什麼呢?

我們知道,在併發場景中同時維護兩個變數的代價是非常大的,往往需要進行加鎖來保證兩個變數的變化是原子性的。而將兩個引數用一個變數維護,便只需一條語句就能保證兩個變數的原子性。這種方式大大降低了使用過程中的併發問題。

有了上面的概念,我們從原始碼層面看看 ThreadPoolExecutor 的幾種狀態,以及 ThreadPoolExecutor 如何同時操作狀態和活躍執行緒數這兩個引數的。

ThreadPoolExecutor 關於狀態初始化的原始碼如下:

```java 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;

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; ```

ThreadPoolExecutor 使用原子 Integer 定義了 ctl 變數。ctl 在一個int中包裝了活躍執行緒數和執行緒池執行時狀態兩個變數。為了達到這樣的目的,ThreadPoolExecutor 的執行緒數被限制在 2^29-1(大約500 million)個,而不是 2^31-1(2 billion)個,因為前3位被用於標識 ThreadPoolExecutor 的狀態。如果未來 ThreadPoolExecutor 中的執行緒數不夠用了,可以把 ctl 設定為原子 long 型別,再調整下相應的掩碼就行了。

COUNT_BITS 概念上用於表示狀態位與執行緒數位的分界值,實際用於狀態變數等移位操作。此處為 Integer.sixze-3=32-3=29。

CAPACITY 表示 ThreadPoolExecutor 的最大容量。由下圖可以看出,經過移位操作後,一個int值的後29位達到最大值:全為1。這29位表示活躍執行緒數,全為1時表明達到 ThreadPoolExecutor 能容納的最大執行緒數。前3位為0,表示該變數只與活躍執行緒數相關,與狀態無關。這也是為了便於後續的位操作。

圖片

RUNNING、SHUTDOWN、STOP、TIDYING、TERMINATED 表示 ThreadPoolExecutor 的5個狀態。這5個狀態對應的可執行操作如下:

RUNNING:可接收新任務,可持續處理阻塞佇列中的任務。

SHUTDOWN:不可接收新任務,可繼續處理阻塞佇列中的任務。

STOP:不可接收新任務,中斷阻塞佇列中所有任務。

TIDYING:所有任務直接終止,所有執行緒清空。

TERMINATED:執行緒池關閉。

這5個狀態的計算過程如下圖所示,經過移位計算後,數值的後29位全為0,前3位分別代表不同的狀態。

圖片

經過以上的變數定義後,ThreadPoolExecutor 將狀態與執行緒數分離,分別設定再一個int值的不同連續位上,這也為下面的操作帶來了極大的便利。

接下來我們來看看 ThreadPoolExecutor 是如何獲取狀態和執行緒數的。

3.1 runStateOf(c)方法

java private static int runStateOf(int c) { return c & ~CAPACITY; }

runStateOf() 方法是用於獲取執行緒池狀態的方法。其中形參 c 一般是 ctl 變數,包含了狀態和執行緒數,runStateOf()移位計算的過程如下圖所示。

圖片

CAPACITY 取反後高三位置1,低29位置0。取反後的值與 ctl 進行 ‘與’ 操作。由於任何值 ‘與’ 1等於原值,‘與’ 0等於0。因此 ‘與’ 操作過後,ctl 的高3位保留原值,低29位置0。這樣就將狀態值從 ctl 中分離出來。

3.2 workerCountOf(c)方法

java private static int workerCountOf(int c) { return c & CAPACITY; }

workerCountOf(c) 方法的分析思路與上述類似,就是把後29位從ctl中分離出來,獲得活躍執行緒數。如下圖所示,這裡就不再贅述。

圖片

3.3 ctlOf(rs, wc)方法

java private static int ctlOf(int rs, int wc) { return rs | wc; }

ctlOf(rs, wc)通過狀態值和執行緒數值計算出 ctl 值。rs是runState的縮寫,wc是workerCount的縮寫。rs的後29位為0,wc的前三位為0,兩者通過 ‘或’ 操作計算出來的最終值同時保留了rs的前3位和wc的後29位,即 ctl 值。

圖片

ThreadPoolExecutor 中還有一些其它操作 ctl 的方法,分析思路與上面都大同小異,大家有興趣可以自己看看。

本小結最後再來看看 ThreadPoolExecutor 狀態轉換的途徑,也可以理解為生命週期。

圖片

四、execute()執行流程

4.1 execute 方法

execute() 原始碼如下所示:

java public void execute(Runnable command) { // 如果待執行的任務為null,直接返回空指標異常。如果任務都沒有,下面的步驟都沒有執行的必要啦。 if (command == null) throw new NullPointerException(); // 獲取 ctl 的值,ctl = (runState + workerCount) int c = ctl.get(); // 如果 workerCount(工作執行緒數) < 核心執行緒數 if (workerCountOf(c) < corePoolSize) { // 執行 addWorker 方法。addWorker()方法會在下面進行詳細分析,這裡可以簡單理解為新增工作執行緒處理任務。這裡的true表示:在小於核心執行緒數時新增worker執行緒,即新增核心執行緒。 if (addWorker(command, true)) // 新增成功則直接返回 return; // 新增失敗,重新獲取 ctl 的值,防止在新增worker時狀態改變 c = ctl.get(); } // 執行到這裡表示核心執行緒數已滿,因此下面addWorker中第二個引數為false。判斷執行緒池是否是執行狀態,如果是則嘗試將任務新增至 任務佇列 中 if (isRunning(c) && workQueue.offer(command)) { // 再次獲取 ctl 的值,進行 double-check int recheck = ctl.get(); // 如果執行緒池為非執行狀態,則嘗試從任務佇列中移除任務 if (! isRunning(recheck) && remove(command)) // 移除成功後執行拒絕策略 reject(command); // 如果執行緒池為執行狀態、或移除任務失敗 else if (workerCountOf(recheck) == 0) // 執行 addWorker 方法,此時新增的是非核心執行緒(空閒執行緒,有存活時間) addWorker(null, false); } // 如果執行緒池是非執行狀態,或者 任務佇列 新增任務失敗,再次嘗試 addWorker() 方法 else if (!addWorker(command, false)) // addWorker() 失敗,執行拒絕策略 reject(command); }

原始碼分析直接看註釋就行了,每一行都有,灰常灰常的詳細了。

從原始碼中可以看到,execute() 方法主要封裝了 ThreadPoolExecutor 建立執行緒的判斷邏輯,核心執行緒和空閒執行緒的建立時機,拒絕策略的執行時機都在該方法進行判斷。這裡通過下面的流程圖對上述原始碼進行總結下。

圖片

通過建立執行緒去執行提交的任務邏輯封裝在 addWorker() 方法中。下一小節我們將來分析執行提交任務的具體邏輯。execute() 方法中還有幾個方法這裡說明下。

3.1.1 workerCountOf()

從 ctl 中獲取活躍執行緒數,在第二小節已經介紹過了。

3.1.2 isRunning()

java private static boolean isRunning(int c) { return c < SHUTDOWN; }

依據 ctl 的值判斷 ThreadPoolExecutor 是否執行狀態。原始碼中直接判斷 ctl < SHUTDOWN 是否成立,這是因為執行狀態下的 ctl 最高位為1,肯定是負數;而其它狀態最高位為0,肯定是正數。因此判斷 ctl 的大小即可判斷是否為執行態。

3.1.3 reject()

java final void reject(Runnable command) { handler.rejectedExecution(command, this); }

直接呼叫初始化時的 RejectedExecutionHandler 介面的 rejectedExecution() 方法。這也是典型的策略模式的使用,真正的拒絕操作被封裝在實現了 RejectedExecutionHandler 介面的實現類中。這裡就不進行展開。

4.2 addWorker 方法

addWorker()原始碼分析如下:

java private boolean addWorker(Runnable firstTask, boolean core) { retry: // 死迴圈執行邏輯。確保多執行緒環境下在預期條件下退出迴圈。 for (;;) { // 獲取 ctl 值並從中提取執行緒池 執行狀態 int c = ctl.get(); int rs = runStateOf(c); // 如果 rs > SHUTDOWN,此時不允許接收新任務,也不允許執行工作佇列中的任務,直接返回fasle。 // 如果 rs == SHUTDOWN,任務為null,並且工作佇列不為空,此時走下面的 '執行工作佇列中任務' 的邏輯。 // 這裡設定 firstTask == null 是因為:執行緒池在SHUTDOWN狀態下,不允許新增新任務,只允許執行工作佇列中剩餘的任務。 if (rs >= SHUTDOWN && ! (rs == SHUTDOWN && firstTask == null && ! workQueue.isEmpty())) return false; for (;;) { // 獲取活躍執行緒數 int wc = workerCountOf(c); // 如果活躍執行緒數 >= 容量,不允許新增新任務 // 如果 core 為 true,表示建立核心執行緒,如果 活躍執行緒數 > 核心執行緒數,則不允許建立執行緒 // 如果 core 為 false,表示建立空閒執行緒,如果 活躍執行緒數 > 最大執行緒數,則不允許建立執行緒 if (wc >= CAPACITY || wc >= (core ? corePoolSize : maximumPoolSize)) return false; // 嘗試增加核心執行緒數,增加成功直接中斷最外層死迴圈,開始建立worker執行緒 // 增加失敗則持續執行迴圈內邏輯 if (compareAndIncrementWorkerCount(c)) break retry; // 獲取 ctl 值,判斷執行狀態是否改變 c = ctl.get(); // 如果執行狀態已經改變,則從重新執行外層死迴圈 // 如果執行狀態未改變,繼續執行內層死迴圈 if (runStateOf(c) != rs) continue retry; } } // 用於記錄worker執行緒的狀態 boolean workerStarted = false; boolean workerAdded = false; Worker w = null; try { // new 一個新的worker執行緒,每一個Worker內持有真正執行任務的執行緒。 w = new Worker(firstTask); final Thread t = w.thread; if (t != null) { // 加鎖,保證workerAdded狀態更改的原子性 final ReentrantLock mainLock = this.mainLock; mainLock.lock(); try { // 獲取執行緒池狀態 int rs = runStateOf(ctl.get()); // 如果為執行狀態,則建立worker執行緒 // 如果為 SHUTDOWN 狀態,並且 firstTask == null,此時將建立執行緒執行 任務佇列 中的任務。 if (rs < SHUTDOWN || (rs == SHUTDOWN && firstTask == null)) { // 如果執行緒在未啟動前就已經執行,丟擲異常 if (t.isAlive()) throw new IllegalThreadStateException(); // 本地快取worker執行緒 workers.add(w); int s = workers.size(); if (s > largestPoolSize) largestPoolSize = s; // worker執行緒新增成功,更改為 true 狀態 workerAdded = true; } } finally { mainLock.unlock(); } // 更改狀態成功後啟動worker執行緒 if (workerAdded) { // 啟動worker執行緒 t.start(); // 更改啟動狀態 workerStarted = true; } } } finally { // 如果工作執行緒狀態未改變,則處理失敗邏輯 if (! workerStarted) addWorkerFailed(w); } return workerStarted; }

addWorker() 通過內外兩層死迴圈判斷 ThreadPoolExecutor 執行狀態並通過CAS成功更新活躍執行緒數。這是為了保證執行緒池中的多個執行緒在併發環境下都能夠按照預期的條件退出迴圈。

隨後方法會 new 一個 Worker 並啟動 Worker 內建的工作執行緒。這裡通過workerAdded和workerStarted兩個狀態判斷 Worker 是否被成功快取與啟動。

修改 workerAdded 過程會使用 ThreadPoolExecutor 的 mainlock 上鎖保證原子性,防止多執行緒併發環境下, 向workers中新增資料以及獲取workers數量這兩個過程出現預期之外的情況。

addWorker() 啟動worker執行緒的步驟是先new一個Worker物件,然後從中獲取工作執行緒,再start,因此真正的執行緒啟動過程還是在Worker物件中。

這裡通過一張流程圖對addWorker總結下:

圖片

addWorker 還有幾個方法也在這裡分析下:

4.2.1 runStateOf()

從 ctl 中獲取 ThreadPoolExecutor 狀態,詳細分析看第二章。

4.2.2 workerCountOf()

從 ctl 中獲取 ThreadPoolExecutor 活躍執行緒數,詳細分析看第二章。

4.2.3 compareAndIncrementWorkerCount()

java int c = ctl.get(); if (compareAndIncrementWorkerCount(c)) {...} private boolean compareAndIncrementWorkerCount(int expect) { return ctl.compareAndSet(expect, expect + 1); }

通過CAS的方式令 ctl 中活躍執行緒數+1。這裡為什麼只要讓 ctl 的值+1就能更改執行緒數了呢?因為 ctl 執行緒數的值儲存在後29位中,在不溢位的情況下,+1只會影響後29位的數值,只會令執行緒數+1。而不影響執行緒池狀態。

4.2.4 addWorkerFailed()

```java private void addWorkerFailed(Worker w) { final ReentrantLock mainLock = this.mainLock; mainLock.lock(); try { if (w != null) // 移除worker workers.remove(w); // 活躍執行緒數-1 decrementWorkerCount(); // 嘗試停止執行緒池 tryTerminate(); } finally { mainLock.unlock(); } }

private void decrementWorkerCount() { do {} while (! compareAndDecrementWorkerCount(ctl.get())); } ```

該方法是在工作執行緒啟動失敗後執行的方法。什麼情況下會出現這種問題呢?在成功增加活躍執行緒數後併成功new Worker後,執行緒池狀態改變為 > SHUTDOWN,既不可接受新任務,又不能執行任務佇列剩餘的任務,此時執行緒池應該直接停止。

該方法就是在這種情況下:

  • 從workers快取池中移除新建立的Worker;

  • 通過死迴圈+CAS確保活躍執行緒數減1;

  • 執行tryTerminate() 方法,嘗試停止執行緒池。

執行完 tryTerminate() 方法後,執行緒池將會進入到 TERMINATED狀態。

4.2.5 tryTerminate()

java final void tryTerminate() { for (;;) { int c = ctl.get(); // 如果當前執行緒池狀態為以下之一,無法直接進入 TERMINATED 狀態,直接返回false,表示嘗試失敗 if (isRunning(c) || runStateAtLeast(c, TIDYING) || (runStateOf(c) == SHUTDOWN && ! workQueue.isEmpty())) return; // 如果活躍執行緒數不為0,中斷所有的worker執行緒,這個會在下面詳細講解,這裡會關係到 Worker 雖然繼承了AQS,但是並未使用裡面的CLH的原因。 if (workerCountOf(c) != 0) { interruptIdleWorkers(ONLY_ONE); return; } // 加上全域性鎖 final ReentrantLock mainLock = this.mainLock; mainLock.lock(); try { // 首先通過 CAS 將 ctl 改變成 (rs=TIDYING, wc=0),因為經過上面的判斷保證了當先執行緒池能夠達到這個狀態。 if (ctl.compareAndSet(c, ctlOf(TIDYING, 0))) { try { // 鉤子函式,使用者可以通過繼承 ThreadPoolExecutor 實現自定義的方法。 terminated(); } finally { // 將 ctl 改變成 (rs=TERMINATED, wc=0),此時執行緒池將關閉。 ctl.set(ctlOf(TERMINATED, 0)); // 喚醒其它執行緒,喚醒其實也沒用了,其它執行緒喚醒後經過判斷得知執行緒池 TERMINATED 後也會退出。 termination.signalAll(); } return; } } finally { // 釋放全域性鎖 mainLock.unlock(); } } }

五、Worker 內建類分析

5.1 Worker物件分析

Worker物件的原始碼分析:

java private final class Worker extends AbstractQueuedSynchronizer implements Runnable { // 工作執行緒 final Thread thread; // 提交的待執行任務 Runnable firstTask; // 已經完成的任務量 volatile long completedTasks; Worker(Runnable firstTask) { // 初始化狀態 setState(-1); this.firstTask = firstTask; // 通過執行緒工廠建立執行緒 this.thread = getThreadFactory().newThread(this); } // 執行提交任務的方法,具體執行邏輯封裝在 runWorker() 中,當addWorker() 中t.start()後,將執行該方法 public void run() { runWorker(this); } // 實現AQS中的一些方法 protected boolean isHeldExclusively() { ... } protected boolean tryAcquire(int unused) { ... } protected boolean tryRelease(int unused) { ... } public void lock() { ... } public boolean tryLock() { ... } public void unlock() { ... } public boolean isLocked() { ... } // 中斷持有的執行緒 void interruptIfStarted() { Thread t; if (getState() >= 0 && (t = thread) != null && !t.isInterrupted()) { try { t.interrupt(); } catch (SecurityException ignore) {} } } }

從上面原始碼可以看出:Worker實現了Runnable介面,說明Worker是一個任務;Worker又繼承了AQS,說明Worker同時具有鎖的性質,但Worker並沒有像ReentrantLock等鎖工具使用了CLH的功能,因為執行緒池中並不存在多個執行緒訪問同一個Worker的場景,這裡只是使用了AQS中狀態維護的功能,這個具體會在下面進行詳細說明。

每個Worker物件會持有一個工作執行緒 thread,在Worker初始化時,通過執行緒工廠建立該工作執行緒並將自己作為任務傳入工作執行緒當中。因此,執行緒池中任務的執行其實並不是直接執行提交任務的run()方法,而是執行Worker中的run()方法,在該方法中再執行提交任務的run()方法。

Worker 中的 run() 方法是委託給 ThreadPoolExecutor 中的 runWorker() 執行具體邏輯。

這裡用一張圖總結下:

  • Worker本身是一個任務,並且持有使用者提交的任務和工作執行緒。

  • 工作執行緒持有的任務是this本身,因此呼叫工作執行緒的start()方法其實是執行this本身的run()方法。

  • this本身的run()委託全域性的runWorker()方法執行具體邏輯。

  • runWorker()方法中執行使用者提交任務的run()方法,執行使用者具體邏輯。

圖片

5.2 runWorker 方法

runWorker() 原始碼如下所示:

java final void runWorker(Worker w) { Thread wt = Thread.currentThread(); // 拷貝提交的任務,並將 Worker 中的 firstTask 置為 null,便於下一次重新賦值。 Runnable task = w.firstTask; w.firstTask = null; w.unlock(); boolean completedAbruptly = true; try { // 執行完持有任務後,通過 getTask() 不斷從任務佇列中獲取任務 while (task != null || (task = getTask()) != null) { w.lock(); try { // ThreadPoolExecutor 的鉤子函式,使用者可以實現 ThreadPoolExecutor,並重寫 beforeExecute() 方法,從而在任務執行前 完成使用者定製的操作邏輯。 beforeExecute(wt, task); Throwable thrown = null; try { // 執行提交任務的 run() 方法 task.run(); } catch (RuntimeException x) { ... } finally { // ThreadPoolExecutor 的鉤子函式,同 beforeExecute,只不過在任務執行完後執行。 afterExecute(task, thrown); } } finally { // 便於任務回收 task = null; w.completedTasks++; w.unlock(); } } completedAbruptly = false; } finally { // 執行到這裡表示任務佇列中沒了任務,或者執行緒池關閉了,此時需要將worker從快取衝清除 processWorkerExit(w, completedAbruptly); } }

runWorker() 是真正執行提交任務的方法,但其並沒有通過Thread.start()方法執行任務,而是直接執行任務的run()方法。

runWorker() 會從任務佇列中不斷獲取任務並執行。

runWorker() 提供了兩個鉤子函式,如果 jdk 的 ThreadPoolExecutor 無法滿足開發人員的需求,開發人員可以繼承 ThreadPoolExecutor並重寫beforeExecute()和afterExecute()方法定製任務執行前需要執行的邏輯。比如設定一些監控指標或者列印日誌等。

5.2.1 getTask()

java private Runnable getTask() { boolean timedOut = false; // 死迴圈保證一定獲取到任務 for (;;) { ... try { // 從任務佇列中獲取任務 Runnable r = timed ? workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) : workQueue.take(); if (r != null) return r; timedOut = true; } catch (InterruptedException retry) { timedOut = false; } } }

5.2.2 processWorkerExit()

java private void processWorkerExit(Worker w, boolean completedAbruptly) { ... final ReentrantLock mainLock = this.mainLock; mainLock.lock(); try { completedTaskCount += w.completedTasks; // 從快取中移除worker workers.remove(w); } finally { mainLock.unlock(); } // 嘗試停止執行緒池 tryTerminate(); ... }

六、shutdown()執行流程

執行緒池擁有兩個主動關閉的方法;

shutdown():關閉執行緒池中所有空閒Worker執行緒,改變執行緒池狀態為SHUTDOWN;

shutdownNow():關閉執行緒池中所有Worker執行緒,改變執行緒池狀態為STOP,並返回所有正在等待處理的任務列表。

這裡為什麼要將Worker執行緒區分為空閒和非空閒呢?

由上面的 runWorker() 方法,我們知道Worker執行緒在理想情況下會在while迴圈中不斷從任務佇列中獲取任務並執行,此時的Worker執行緒就是非空閒的;沒有在執行任務的worker執行緒則是空閒的。因為執行緒池的SHUTDOWN狀態不允許接收新任務,只允許執行任務佇列中剩餘的任務,因此需要中斷所有空閒的Worker執行緒,非空閒執行緒則持續執行任務佇列的任務,直至佇列為空。而執行緒池的STOP狀態既不允許接受新任務,也不允許執行剩餘的任務,因此需要關閉所有Worker執行緒,包括正在執行的。

6.1 shutdown()

shutdown() 原始碼如下:

java public void shutdown() { // 上全域性鎖 final ReentrantLock mainLock = this.mainLock; mainLock.lock(); try { // 校驗是否有關閉執行緒池的許可權,這裡主要通過 SecurityManager 校驗當前執行緒與每個 Worker 執行緒的 “modifyThread” 許可權 checkShutdownAccess(); // 修改執行緒池狀態 advanceRunState(SHUTDOWN); // 關閉所有空閒執行緒 interruptIdleWorkers(); // 鉤子函式,使用者可以繼承 ThreadPoolExecutor 並實現自定義鉤子,ScheduledThreadPoolExecutor便實現了自己的鉤子函式 onShutdown(); } finally { mainLock.unlock(); } // 嘗試關閉執行緒池 tryTerminate(); }

shutdown() 將 ThreadPoolExecutor 的關閉步驟封裝在幾個方法中,並且通過全域性鎖保證只有一個執行緒能主動關閉 ThreadPoolExecutor。ThreadPoolExecutor 同樣提供了一個鉤子函式 onShutdown() 讓開發人員定製化關閉過程。比如ScheduledThreadPoolExecutor 就會在關閉時對任務佇列進行清理。

下面對其中的方法進行分析。

checkShutdownAccess()

```java private static final RuntimePermission shutdownPerm = new RuntimePermission("modifyThread");

private void checkShutdownAccess() { SecurityManager security = System.getSecurityManager(); if (security != null) { // 校驗當前執行緒的許可權,其中 shutdownPerm 就是一個具有 modifyThread 引數的 RuntimePermission 物件。 security.checkPermission(shutdownPerm); final ReentrantLock mainLock = this.mainLock; mainLock.lock(); try { for (Worker w : workers) // 校驗所有worker執行緒是否具有 modifyThread 許可權 security.checkAccess(w.thread); } finally { mainLock.unlock(); } } } ```

advanceRunState()

java // targetState = SHUTDOWN private void advanceRunState(int targetState) { for (;;) { int c = ctl.get(); // 判斷當前執行緒池狀態 >= SHUTDOWN是否成立,如果不成立的話,通過CAS進行修改 if (runStateAtLeast(c, targetState) || ctl.compareAndSet(c, ctlOf(targetState, workerCountOf(c)))) break; } } private static boolean runStateAtLeast(int c, int s) { return c >= s; }

該方法中判斷線當前程池狀態 >= SHUTDOWN 是否成立其實也是用到了之前執行緒池狀態定義的技巧。對於非執行狀態的其它狀態都為正數,且高三位都不同,TERMINATED(011) > TIDYING(010) > STOP(001) > SHUTDOWN(000)而高三位的大小取決了整個數的大小。因此對於不同狀態,無論活躍執行緒數是多少,執行緒池的狀態始終決定著 ctl 值的大小。即TERMINATED 狀態下的 ctl 值 > TIDYING 狀態下的 ctl 值恆成立。

interruptIdleWorkers()

java 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; // 判斷worker執行緒是否已經被標記中斷了,如果沒有,則嘗試獲取worker執行緒的鎖 if (!t.isInterrupted() && w.tryLock()) { try { // 中斷執行緒 t.interrupt(); } catch (SecurityException ignore) { } finally { w.unlock(); } } // 如果 onlyOne 為true的話最多中斷一個執行緒 if (onlyOne) break; } } finally { mainLock.unlock(); } }

剛方法會嘗試獲取Worker的鎖,只有獲取成功的情況下才會中斷執行緒。這裡也與前面說的Worker雖然繼承了AQS但卻沒使用CLH有關,後面會進行分析。

tryTerminate() 方法已經在前面分析過了,這裡不過多敘述。

6.2 shutdownNow()

java 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(); } // 嘗試關閉執行緒池 tryTerminate(); // 返回任務列表 return tasks; }

該方法與 shutdown() 比較相似,都將核心步驟封裝在了幾個方法中,其中 checkShutdownAccess() 和 advanceRunState() 相同。下面對不同的方法進行說明

interruptWorkers()

java private void interruptWorkers() { final ReentrantLock mainLock = this.mainLock; mainLock.lock(); try { // 遍歷所有的Worker,只要Worker啟動了就將其中斷 for (Worker w : workers) w.interruptIfStarted(); } finally { mainLock.unlock(); } } void interruptIfStarted() { Thread t; // state >= 0表示worker已經啟動,Worker啟動並且持有執行緒不為null並且持有執行緒未被標記中斷,則中斷該執行緒 if (getState() >= 0 && (t = thread) != null && !t.isInterrupted()) { try { t.interrupt(); } catch (SecurityException ignore) { } } }

該方法並沒有嘗試去獲取Worker的鎖,而是直接中斷執行緒。因為STOP狀態下的執行緒池不允許處理任務佇列中正在等待的任務。

drainQueue()

java // 將任務佇列中的任務新增進列表中返回,通常情況下使用 drainTo() 就行了,但如果佇列是延遲佇列或是其他無法通過drainTo()方法轉移任務時,再通過迴圈遍歷進行轉移 private List<Runnable> drainQueue() { ... }

七、Worker繼承AQS的原因

首先說結論——Worker繼承AQS是為了使用其中狀態管理的功能,並沒有像ReentrantLock使用AQS中CLH的性質。

我們先來看看Worker中與AQS相關的方法:

java // 引數為unused,從命名也可以知道該引數未被使用 protected boolean tryAcquire(int unused) { // 通過CAS改變將狀態由0改變為1 if (compareAndSetState(0, 1)) { // 設定當前執行緒獨佔 setExclusiveOwnerThread(Thread.currentThread()); return true; } return false; } // 該方法只在 runWorker() 中被使用 public void lock() { acquire(1); } public boolean tryLock() { return tryAcquire(1); }

Worker中的tryAcquire只是將狀態改為1,而引數未被使用,因此我們可以斷定,Worker中的狀態可能取值為(0, 1)。這裡沒有考慮初始化狀態-1是避免出現混淆。

再看 lock() 方法,lock() 方法被呼叫的唯一位置就是在 runWorker() 中啟動worker執行緒前。而 runWorker() 是通過 Worker 中的 run() 呼叫的。Worker 作為任務只被傳遞給本身持有的工作執行緒中,因此 Worker 中的 run() 方法只能被本身持有的工作執行緒通過 start() 呼叫,因此 runWorker() 只會被 Worker 本身持有的工作執行緒所呼叫,lock() 方法也只會被單執行緒呼叫,不存在多個執行緒競爭同一把鎖的情況,也就不存在多執行緒環境下,只有一個執行緒能獲得鎖導致其他等待執行緒被新增進CLH佇列的情況。所以 Worker 並沒沒有使用CLH的功能。

這也就很好說明了 tryAcquire() 方法並沒有使用傳遞的引數,因為Worker只存在兩種狀態,要麼被上鎖(非空閒,state=1),要麼未被上鎖(空閒,state=0)。無需通過傳遞引數設定其他的狀態。

java final void runWorker(Worker w) { ... try { while (task != null || (task = getTask()) != null) { // 唯一被呼叫的地方 w.lock(); ... } } }

以上分析說明了 Worker 沒有使用 AQS 的 CLH 功能。那麼 Worker 是如何使用狀態管理的功能的呢?

在關閉執行緒池的 shutdown() 方法中,有一個步驟是中斷所有的空閒 Worker 執行緒。而在中斷所有 Worker 執行緒前會判斷 Worker 執行緒是否能被獲取到鎖,通過 tryLock() -> tryAcquire() 判斷 Worker 的狀態是否為0,只有能夠獲取到鎖的 Worker 才會被中斷,而能被獲取到鎖的 Worker 即為空閒 Worker(state=0)。而不能被獲取到鎖的 Worker 表名已經執行過 lock() 方法了,此時 Worker 在 While 迴圈不斷獲取阻塞佇列的任務執行,在shutdown()方法中不能被中斷。

java private void interruptIdleWorkers(boolean onlyOne) { ... try { for (Worker w : workers) { Thread t = w.thread; if (!t.isInterrupted() && w.tryLock()) { ... } } } }

因此 Worker 的狀態管理其實是通過 state 的值(0 或 1)判斷 Worker 是否為空閒的,如果是空閒的,則可以線上程池關閉時被中斷掉,否則得一直在while迴圈中獲取阻塞佇列中的任務並執行,直至佇列中任務為空後才被釋放。如下圖所示:

圖片

八、拒絕策略

本章只討論 ThreadPoolExecutor 內建的四個拒絕策略 handler。

8.1 CallerRunsPolicy

java public static class CallerRunsPolicy implements RejectedExecutionHandler { public CallerRunsPolicy() { } public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { // 如果執行緒池未被關閉,直接在當前執行緒中執行任務 if (!e.isShutdown()) { r.run(); } } }

直接在呼叫執行緒中執行被拒絕的任務。只要執行緒池為 RUNNING 狀態,任務仍被執行。如果為非 RUNNING 狀態,任務將直接被忽略,這也符合執行緒池狀態的行為。

8.2 AbortPolicy

java public static class AbortPolicy implements RejectedExecutionHandler { public AbortPolicy() { } public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { // 丟擲拒絕異常 throw new RejectedExecutionException("Task " + r.toString() + " rejected from " + e.toString()); } }

任務被拒絕後直接丟擲拒絕異常。

8.3 DiscardPolicy

java public static class DiscardPolicy implements RejectedExecutionHandler { public DiscardPolicy() { } // 空方法,什麼都不執行 public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { } }

拋棄該任務。拒絕方法為空,表示什麼都不執行,等同於將任務拋棄。

8.4 DiscardOldestPolicy

java public static class DiscardOldestPolicy implements RejectedExecutionHandler { public DiscardOldestPolicy() { } public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { if (!e.isShutdown()) { // 從阻塞佇列中獲取(移除)隊頭的任務, e.getQueue().poll(); // 再次嘗試execute當前任務 e.execute(r); } } }

移除阻塞佇列中最早進入佇列中(隊頭)的任務,然後再次嘗試執行execute()方法,將當前任務入隊。這是典型的喜新厭舊的策略。

九、ThreadPoolExecutor二次開發實踐

介紹完了 ThreadPoolExecutor 的核心原理,我們來看看 vivo 自研的 NexTask 併發框架是如何玩轉執行緒池並提升業務人員的開發速度和程式碼執行速度。

NexTask 對業務常用模式、演算法、場景進行抽象化,以元件的形式落地。它提供了一個快速、輕量級、簡單易用並且遮蔽了底層技術細節的方式,能夠讓開發人員快速編寫併發程式,更大程度上為開發賦能。

首先給出 NexTask 架構圖,然後我們針對架構圖中使用到了 ThreadPoolExecutor 的地方進行詳細分析。

圖片

java // Executor部分程式碼: public class Executor { ... private static DefaultTaskProcessFactory taskProcessFactory = new DefaultTaskProcessFactory(); // 對外提供的API,使用者快速建立任務處理器 public static TaskProcess getCommonTaskProcess(String name) { return TaskProcessManager.getTaskProcess(name, taskProcessFactory); } public static TaskProcess getTransactionalTaskProcess(String name) { return TaskProcessManager.getTaskProcessTransactional(name, taskProcessFactory); } ... }

Executor 是對外提供的介面,開發人員可以使用它具備的簡單易用的API,快速通過工作管理員 TaskProcessManager 建立任務處理器 TaskProcess。

java // TaskProcessManager 部分程式碼: public class TaskProcessManager { // 快取map,<業務名稱, 針對該業務的任務處理器> private static Map<String, TaskProcess> taskProcessContainer = new ConcurrentHashMap<String, TaskProcess>(); ... }

TaskProcessManager 持有一個 ConcurrentHashMap 本地快取有所的任務處理器,每個任務處理器與特定的業務名稱一一對映。在獲取任務處理器時,通過具體的業務名稱從快取中獲取,不僅能夠保證各個業務間的任務處理相互隔離,同時能夠防止多次建立、銷燬執行緒池造成的資源損耗。

java // TaskProcess 部分程式碼: public class TaskProcess { // 執行緒池 private ExecutorService executor; // 執行緒池初始化 private void createThreadPool() { executor = new ThreadPoolExecutor(coreSize, poolSize, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(2048), new DefaultThreadFactory(domain), new ThreadPoolExecutor.AbortPolicy()); } // 多執行緒提交任務進行處理 public <T> List<T> executeTask(List<TaskAction<T>> tasks) { int size = tasks.size(); // 建立一個與任務數相同的 CountDownLatch,保證所有任務全部處理完後一起返回結果 final CountDownLatch latch = new CountDownLatch(size); // 返回結果初始化 List<Future<T>> futures = new ArrayList<Future<T>>(size); List<T> resultList = new ArrayList<T>(size); // 遍歷所有任務,提交到執行緒池 for (final TaskAction<T> runnable : tasks) { Future<T> future = executor.submit(new Callable<T>() { @Override public T call() throws Exception { // 處理具體的任務邏輯 try { return runnable.doInAction(); } // 處理完成後,CountDownLatch - 1 finally { latch.countDown(); } } }); futures.add(future); } try { // 等待所有任務處理完成 latch.await(50, TimeUnit.SECONDS); } catch (Exception e) { log.info("Executing Task is interrupt."); } // 封裝結果並返回 for (Future<T> future : futures) { try { T result = future.get();// wait if (result != null) { resultList.add(result); } } catch (Exception e) { throw new RuntimeException(e); } } return resultList; } ... }

每個TaskProcess都持有一個執行緒池,由執行緒池的初始化過程可以看到,TaskProcess 採用的是有界阻塞佇列,佇列中最多存放2048個任務,一旦超過這個數量後,將會直接拒絕接收任務並丟擲拒絕處理異常。

TaskProcess 會遍歷使用者提交的任務列表,並通過 submit() 方法將其提交至執行緒池處理,submit() 底層其實還是呼叫的 ThreadPoolExecutor#execute() 方法,只不過會在呼叫前將任務封裝成 RunnableFuture,這裡就是FutureTask框架的內容了,就不進行展開。

TaskProcess會在每次處理任務時,建立一個 CountDownLatch,並在任務結束後執行 CountDownLatch.countDown(),這樣就能保證所有任務在執行完成阻塞當前執行緒,直至所有任務處理完後統一獲取結果並返回。

十、總結

JDK雖然為開發人員提供了Executors工具類以及內建的多種執行緒池,但那些執行緒池的使用非常侷限,無法滿足日益複雜的業務場景。阿里官方的程式設計規約中也推薦開發人員不要直接使用JDK自帶的執行緒池,而是根據自身業務場景通過ThreadPoolExecutor進行建立執行緒池。因此,瞭解ThreadPoolExecutor內部原理對日常開發中熟練使用執行緒池也是至關重要的。

本文主要是對ThreadPoolExecutor內部核心原理進行探究,介紹了其構造方法及其各個構造引數的詳細意義,以及執行緒池核心 ctl 引數的轉化方法。隨後花了大量篇幅深入ThreadPoolExecutor原始碼介紹執行緒池的啟動與關閉流程、核心內建類Worker等。ThreadPoolExecutor還有其他方法本文暫未介紹,讀者可以在讀完本文的基礎上自行閱讀其他原始碼,相信會有一定幫助。

作者:vivo網際網路伺服器團隊-Xu Weiteng