執行緒池 ThreadPoolExecutor
介紹
執行緒池主要解決兩個問題:一是當執行大量非同步任務時執行緒池能夠提供較好的效能。在不使用執行緒池時,每當需要執行任務時就需要 new 一個執行緒來執行,頻繁的建立與銷燬非常消耗效能。而執行緒池中的執行緒是可以複用的,不需要在每次需要執行任務時候都重新建立和銷燬。二是執行緒池提供了資源限制和管理的手段,比如可以限制執行緒個數,動態增加執行緒等。
另外,執行緒池也提供了許多可調引數和可擴充套件性介面,以滿足不同情況下的需要,我們可以使用更方便的 Executors 的工廠方法,來建立不同型別的執行緒池,也可以自己自定義執行緒池。
執行緒池的工作機制
- 執行緒池剛建立的時候沒有任何執行緒,當來了新的請求的時候才會建立
核心執行緒
去處理對應的請求 - 當處理完成之後,核心執行緒並不會回收
- 在核心執行緒達到指定的數量之前,每一個請求都會線上程池中建立一個新的核心執行緒
- 當核心執行緒全都被佔用的時候,新來的請求會放入工作佇列中。工作佇列本質上是一個
阻塞佇列
- 當工作佇列被佔滿,再來的新請求會交給臨時執行緒來處理
- 臨時執行緒在使用完成之後會繼續存活一段時間,直到沒有請求處理才會被銷燬
類圖介紹
如上類圖所示,Executors 是一個工具類,提供了多種靜態方法,根據我們選擇的不同提供不同的執行緒池例項。
ThreadPoolExecutor 繼承了 AbstractExecutorService 抽象類,在 ThreadPoolExecutor 中成員變數 ctl 是一個 Integer 的原子性變數,用來記錄執行緒池的狀態和執行緒中執行緒個數,類似於 ReentrantReadWriteLock 使用一個變數來儲存兩種資訊一樣。
假設 Integer 型別是 32 位二進位制表示,則其中高 3 位表示執行緒池的狀態,後 29 為表示執行緒池執行緒數量。
//預設RUNNING狀態,執行緒個數為0
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
獲取高 3 位,執行狀態
private static int runStateOf(int c) { return c & ~CAPACITY; }
獲取低 29 位,執行緒個數
private static int workerCountOf(int c) { return c & CAPACITY; }
執行緒池狀態含義如下:
RUNNING
:接受新任務並且處理阻塞佇列中的任務SHUTDOWN
:拒絕新任務但是處理阻塞佇列中的任務STOP
:拒絕新任務並且拋棄阻塞佇列裡的任務,同時中斷正在處理的任務TIDYING
:所有任務都執行完(包括阻塞佇列中的任務)後當前執行緒池活動執行緒數量為 0,將呼叫 terminated 方法TERMINATED
:終止狀態。terminated 呼叫完成方法後的狀態
執行緒池狀態轉換如下:
- RUNNING -> SHUTDOWN:顯式呼叫 shutdown 方法,或者隱式呼叫了 finalize 方法裡面的 shutdown 方法
- RUNNING 或 SHUTDOWN -> STOP :顯式呼叫 shutdownNow 方法時
- SHUTDOWN -> TIDYING:當執行緒池和任務佇列都為空時
- STOP -> TIDYING:當執行緒池為空時
- TIDYING -> TERMINATED:當 terminated hook 方法執行完成時
執行緒池引數如下:
引數名 | 型別 | 含義 |
---|---|---|
corePoolSize | int | 核心執行緒數 |
maxPoolSize | int | 最大執行緒數 |
keepAliveTime | long | 保持存活時間 |
workQueue | BlockingQueue | 任務儲存佇列 |
threadFactory | ThreadFactory | 當執行緒池需要新的執行緒時,使用 ThreadFactory 來建立新的執行緒 |
Handler | RejectedExecutionHandler | 由於執行緒池無法接受所提交的任務所給出的拒絕策略 |
corePoolSize
:指的是核心執行緒數,執行緒池初始化完成後,預設情況下,執行緒池並沒有任何執行緒,執行緒池會等待任務到來時,再建立新的執行緒去執行任務。maxPoolSize
:執行緒池有可能會在核心執行緒數上,額外增加一些執行緒,但是這些新增加的執行緒有一個上限,最大不能超過 maxPoolSize。- 如果執行緒數小於 corePoolSize,即使其他工作執行緒處於空閒狀態,也會建立一個新的執行緒來執行任務。
- 如果執行緒數大於等於 corePoolSize 但少於 maxPoolSize,則將任務放進工作佇列中。
- 如果佇列已滿,並且執行緒數小於 maxPoolSize,則建立一個新執行緒來執行任務。
- 如果佇列已滿,並且執行緒數已經大於等於 maxPoolSize,則使用拒絕策略來拒絕該任務。
keepAliveTime
:一個執行緒如果處於空閒狀態,並且當前的執行緒數量大於 corePoolSize,那麼在指定時間後,這個空閒執行緒會被銷燬,這裡的指定時間由 keepAliveTime 來設定。workQueue
:新任務被提交後,會先進入到此工作佇列中,任務排程時再從佇列中取出任務。jdk 中提供了四種工作佇列:ArrayBlockingQueue
:基於陣列的有界阻塞佇列
,按 FIFO 排序。新任務進來後,會放到該佇列的隊尾,有界的陣列可以防止資源耗盡問題。當執行緒池中執行緒數量達到 corePoolSize 後,再有新任務進來,則會將任務放入該佇列的隊尾,等待被排程。如果佇列已經是滿的,則建立一個新執行緒,如果執行緒數量已經達到 maxPoolSize,則會執行拒絕策略。LinkedBlockingQueue
:基於連結串列的無界阻塞佇列
(其實最大容量為 Interger.MAX),按照 FIFO 排序。由於該佇列的近似無界性,當執行緒池中執行緒數量達到 corePoolSize 後,再有新任務進來,會一直存入該佇列,而不會去建立新執行緒直到 maxPoolSize,因此使用該工作佇列時,引數 maxPoolSize 其實是不起作用的。SynchronousQueue
:一個不快取任務的阻塞佇列
,生產者放入一個任務必須等到消費者取出這個任務。也就是說新任務進來時,不會快取,而是直接被排程執行該任務,如果沒有可用執行緒,則建立新執行緒,如果執行緒數量達到 maxPoolSize,則執行拒絕策略。PriorityBlockingQueue
:具有優先順序的無界阻塞佇列
,優先順序通過引數 Comparator 實現。delayQueue
:具有優先順序的延時無界阻塞佇列
。LinkedTransferQueue
:基於連結串列的無界阻塞佇列
。LinkedBlockingDeque
:基於連結串列的雙端阻塞佇列
。
threadFactory
:建立一個新執行緒時使用的工廠,可以用來設定執行緒名、是否為 daemon 執行緒等等handler
:當工作佇列中的任務已到達最大限制,並且執行緒池中的執行緒數量也達到最大限制,這時如果有新任務提交進來,就會執行拒絕策略。
如上 ThreadPoolExecutor 類圖所示,其中 mainLock 是獨佔鎖,用來控制新增 Worker 執行緒操作的原子性。termination 是該鎖對應的條件佇列,線上程呼叫 awaitTermination 時用來存放阻塞的執行緒。
private final ReentrantLock mainLock = new ReentrantLock();
private final HashSet<Worker> workers = new HashSet<Worker>();
private final Condition termination = mainLock.newCondition();
Worker 類繼承 AQS 和 Runnable 介面,是具體承載任務的物件。Worker 繼承了 AQS,實現了簡單的不可重入的獨佔鎖,state = 0
表示鎖未被獲取,state = 1
則表示鎖已經被獲取,state = -1
是建立 Worker 時預設的狀態,建立時狀態設定為-1 是為了避免該執行緒在執行 runWorker 方法前被中斷。其中變數 firstTask 記錄該工作執行緒執行的第一個任務,thread 是具體執行任務的執行緒。
private final class Worker extends AbstractQueuedSynchronizer implements Runnable{
final Thread thread;
Runnable firstTask;
//...
DefaultThreadFactory 是執行緒工廠,newThread 方法是對執行緒的一個修飾。其中 poolNumber 是個靜態的原子變數,用來統計執行緒工廠的個數,threadNumber 用來記錄每個執行緒工廠建立了多少執行緒,這兩個值也作為執行緒池和執行緒的名稱的一部分。
原始碼解析
execute 方法
execute 方法主要作用就是提交任務 command 到執行緒池中進行執行。
該圖可以看到,ThreadPoolExecutor 實現其實就是一個生產者消費者模型,當使用者新增任務到執行緒池相當於生產者生產元素,workers 執行緒中的執行緒直接執行任務或者從任務佇列裡面獲取任務則相當於是消費者消費元素。
具體程式碼如下:
public void execute(Runnable command) {
//1. 校驗任務是否為null
if (command == null)
throw new NullPointerException();
//2. 獲取當前執行緒池的狀態+執行緒個數的組合值
int c = ctl.get();
//3. 判斷執行緒池中執行緒個數是否小於corePoolSize,小則開啟新執行緒(core執行緒)執行
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
//4. 如果執行緒池處於RUNNING狀態,則新增任務到阻塞佇列
if (isRunning(c) && workQueue.offer(command)) {
//4.1 二次檢查
int recheck = ctl.get();
//4.2 如果當前執行緒池狀態不是RUNNING則從佇列中刪除任務,並執行拒絕策略
if (! isRunning(recheck) && remove(command))
reject(command);
//4.3 如果當前執行緒池為空,則新增一個執行緒
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
//5. 如果佇列滿,則新增執行緒,新增失敗則執行拒絕策略
else if (!addWorker(command, false))
reject(command);
}
如果當前執行緒池執行緒個數大於等於 corePoolSize 則執行程式碼(4),如果當前執行緒處於 RUNNING 則新增到任務佇列。
需要注意的是這裡判斷執行緒池狀態是因為有可能執行緒池已經處於非 RUNNING 狀態,在非 RUNNING 狀態下是要拋棄新任務的。
如果任務新增成功,則執行程式碼(4.2)進行二次校驗,因為在執行 4.2 之前可能執行緒池的狀態傳送變化,如果執行緒池狀態不是 RUNNING 則把任務從任務佇列中移除,然後執行拒絕策略;如果二次校驗通過,則重新判斷執行緒池裡是否還有執行緒,沒有則新增一個執行緒。
如果程式碼(4)新增任務失敗,則說明佇列已滿,隨後執行程式碼(5)嘗試新增執行緒也就是上圖中的 thread3,thread4 執行緒來執行任務,如果當前執行緒池個數 > maximumPoolSize 則執行拒絕策略。
我們接下來看 addWorker 方法:
private boolean addWorker(Runnable firstTask, boolean core) {
retry:
// 每次for迴圈都需要獲取最新的ctl值
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);
//1. 檢查佇列是否只在必要時為空
if (rs >= SHUTDOWN &&
! (rs == SHUTDOWN &&
firstTask == null &&
! workQueue.isEmpty()))
return false;
//2. 迴圈CAS增加執行緒個數
for (;;) {
int wc = workerCountOf(c);
//2.1 如果執行緒個數超限制則返回false
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maximumPoolSize))
return false;
//2.2 通過CAS增加執行緒個數
if (compareAndIncrementWorkerCount(c))
break retry;
//2.3 CAS失敗後,檢視執行緒狀態是否發生變化,如果變化則跳轉到外層迴圈重新嘗試獲取執行緒池狀態,否則內層迴圈重新進行CAS
c = ctl.get();
if (runStateOf(c) != rs)
continue retry;
}
}
//3. 執行到這一步說明CAS成功
boolean workerStarted = false;
boolean workerAdded = false;
Worker w = null;
try {
//3.1 建立Worker
w = new Worker(firstTask);
final Thread t = w.thread;
if (t != null) {
//3.2 增加獨佔鎖,實現同步,因為可能多個執行緒同時呼叫執行緒池的execute方法
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
//3.3 重新檢查執行緒池狀態,避免在獲取鎖前呼叫了shutdown
int rs = runStateOf(ctl.get());
if (rs < SHUTDOWN ||
(rs == SHUTDOWN && firstTask == null)) {
if (t.isAlive()) // precheck that t is startable
throw new IllegalThreadStateException();
//3.4 新增任務
workers.add(w);
// 更新當前最大執行緒數量 maximumPoolSize 和 corePoolSize可以線上程池建立之後動態修改的
int s = workers.size();
if (s > largestPoolSize)
largestPoolSize = s;
workerAdded = true;
}
} finally {
mainLock.unlock();
}
//3.5 新增成功後啟動任務
if (workerAdded) {
t.start();
workerStarted = true;
}
}
} finally {
// 如果沒有執行過t.start() 就要把這個woker從workers裡面刪除,並且ctl裡面worker數量減1
if (! workerStarted)
addWorkerFailed(w);
}
return workerStarted;
}
首先明確第一部分雙重迴圈目的是通過 CAS 操作進行新增執行緒數,第二部分主要通過 ReentrantLock 安全的將任務新增到 workers 裡,隨後啟動任務。
首先看第一部分程式碼(1)。
if (rs >= SHUTDOWN &&
! (rs == SHUTDOWN && //(1)
firstTask == null && //(2)
! workQueue.isEmpty())) //(3)
程式碼(1)中會在下面三種情況返回 false:
- (1)當前執行緒池狀態為 STOP、TIDYING 或 TERMINATED
- (2)當前執行緒池狀態為 SHUTDOWN 並且已經有了第一個任務
- (3)當前執行緒池狀態為 SHUTDOWN 並且任務佇列為空
程式碼(2)內層迴圈的作用是使用 CAS 操作增加執行緒數量。
執行到程式碼(8)時,說明已經通過 CAS 成功增加了執行緒個數,現在任務還沒有開始執行,所以這部分程式碼通過全域性鎖控制來增加 Worker 到工作集合 workers 中。
工作執行緒 Worker 的執行
使用者執行緒提交到執行緒池之後,由 Worker 來執行,下面是 Worker 的建構函式。
Worker(Runnable firstTask) {
setState(-1); // inhibit interrupts until runWorker
this.firstTask = firstTask;
this.thread = getThreadFactory().newThread(this);//建立一個執行緒
}
在建構函式內首先設定 Worker 的狀態為-1,為了避免當前 Worker 在呼叫 runWorker 方法前被中斷(當其他執行緒呼叫了執行緒池的 shutdownNow 時,如果 Worker 狀態>=0 則會中斷該執行緒)。這裡設定了執行緒的狀態為-1,所以該執行緒就不會被中斷了。
在 runWorker 程式碼中,執行程式碼(1)時會呼叫 unlock 方法,該方法把 status 設定為了 0,所以這時候呼叫 shutdownNow 會中斷 Worker 執行緒。
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;
w.firstTask = null;
w.unlock(); //1. 將state設定為0,允許中斷
boolean completedAbruptly = true;
try {
//2.
while (task != null || (task = getTask()) != null) {
//2.1
w.lock();
////如果狀態值大於等於STOP且當前執行緒還沒有被中斷,則主動中斷執行緒
if ((runStateAtLeast(ctl.get(), STOP) ||
(Thread.interrupted() &&
runStateAtLeast(ctl.get(), STOP))) &&
!wt.isInterrupted())
wt.interrupt();
try {
//2.2 執行任務前處理操作,預設是一個空實現;在子類中可以通過重寫來改變任務執行前的處理行為
beforeExecute(wt, task);
Throwable thrown = null;
try {
//2.3 執行任務
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 {
//2.4 任務之後處理,同beforeExecute
afterExecute(task, thrown);
}
} finally {
task = null;
//2.5 統計當前worker完成了多少個任務
w.completedTasks++;
w.unlock();
}
}
//設定為false,表示任務正常處理完成
completedAbruptly = false;
} finally {
//3. 清理工作
processWorkerExit(w, completedAbruptly);
}
}
這裡在執行具體任務期間加鎖,是為了避免在任務執行期間,其他執行緒呼叫了 shutdown 後正在執行的任務被中斷(shutdown 只會中斷當前被阻塞掛起的執行緒)
清理工作程式碼如下:
private void processWorkerExit(Worker w, boolean completedAbruptly) {
// 如果completedAbruptly為true則表示任務執行過程中丟擲了未處理的異常
// 所以還沒有正確地減少worker計數,這裡需要減少一次worker計數
if (completedAbruptly)
decrementWorkerCount();
//1. 統計執行緒池中完成任務的個數,並從工作集合裡面刪除當前Worker
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
completedTaskCount += w.completedTasks;
workers.remove(w);
} finally {
mainLock.unlock();
}
//1.2 嘗試設定執行緒池狀態為TERMINATED,在關閉執行緒池時等到所有worker都被回收後再結束執行緒池
tryTerminate();
//1.3 如果執行緒池狀態 < STOP,即RUNNING或SHUTDOWN,則需要考慮建立新執行緒來代替被銷燬的執行緒
int c = ctl.get();
if (runStateLessThan(c, STOP)) {
// 如果worker是正常執行完的,則要判斷一下是否已經滿足了最小執行緒數要求
// 否則直接建立替代執行緒
if (!completedAbruptly) {
int min = allowCoreThreadTimeOut ? 0 : corePoolSize;
if (min == 0 && ! workQueue.isEmpty())
min = 1;
if (workerCountOf(c) >= min)
return; // replacement not needed
}
// 重新建立一個worker來代替被銷燬的執行緒
addWorker(null, false);
}
}
在如上程式碼中,程式碼(1.1)統計執行緒池完成任務個數,並且在統計前加了全域性鎖。把在當前工作執行緒中完成的任務累加到全域性計數器,然後從工作集中刪除當前 Worker。
程式碼(1.2)判斷如果當前執行緒池狀態是 SHUTDOWN 並且工作佇列為空,或者當前執行緒池狀態是 STOP 並且當前執行緒池裡面沒有活動執行緒,則設定執行緒池狀態為 TERMINATED。如果設定為了 TERMINATED 狀態,則還需要呼叫條件變數 termination 的 signalAll 方法啟用所有因為呼叫執行緒池的 awaitTermination 方法而被阻塞的執行緒。
程式碼(1.3)則判斷當前執行緒池裡面執行緒個數是否小於核心執行緒個數,如果是則新增一個執行緒。
shutdown 方法
呼叫 shutdown 方法後,執行緒池就不會再接受新的任務了,但是工作佇列裡面的任務還是要執行的。該方法會立刻返回,並不等待佇列任務完成再返回。
public void shutdown() {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
//1. 許可權檢查
checkShutdownAccess();
//2. 設定當前執行緒池狀態為SHUTDOWN,如果已經是SHUTDOWN則直接返回
advanceRunState(SHUTDOWN);
//3. 設定中斷標誌
interruptIdleWorkers();
onShutdown(); // hook for ScheduledThreadPoolExecutor
} finally {
mainLock.unlock();
}
//4. 嘗試將狀態改為TERMINATED
tryTerminate();
}
首先檢視當前呼叫的執行緒是否有關閉執行緒的許可權。
隨後程式碼(2)的程式碼如下。如果當前執行緒池狀態 >= SHUTDOWN 則直接返回,否則設定為 SHUTDOWN 狀態。
private void advanceRunState(int targetState) {
for (;;) {
int c = ctl.get();
if (runStateAtLeast(c, targetState) ||
ctl.compareAndSet(c, ctlOf(targetState, workerCountOf(c))))
break;
}
}
程式碼(3)的原始碼如下,其設定所有空閒執行緒的中斷標誌。這裡首先加了全域性鎖,同時只有一個執行緒可以呼叫 shutdown 方法設定中斷標誌。然後嘗試獲取 Worker 自己的鎖,獲取成功則設定中斷標誌。由於正在執行的任務已經獲取了鎖,所以正在執行的任務沒有被中斷。這裡中斷的是阻塞到 getTask 方法並企圖從佇列裡面獲取任務的執行緒,也就是空閒執行緒。
private void interruptIdleWorkers(boolean onlyOne) {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
for (Worker w : workers) {
Thread t = w.thread;
if (!t.isInterrupted() && w.tryLock()) {
try {
t.interrupt();
} catch (SecurityException ignore) {
} finally {
w.unlock();
}
}
if (onlyOne)
break;
}
} finally {
mainLock.unlock();
}
}
最後嘗試將狀態改為 TERMINATED,首先使用 CAS 設定當前執行緒池狀態為 TIDYING,如果設定成功則執行擴充套件介面 terminated 線上程池狀態變為 TERMINATED 前做一些事情,然後設定當前執行緒池狀態為 TERMINATED。最後呼叫 termination.signalAll 啟用因呼叫條件變數 termination 的 await 系列方法而被阻塞的所有執行緒。
final void tryTerminate() {
for (;;) {
int c = ctl.get();
if (isRunning(c) ||
runStateAtLeast(c, TIDYING) ||
(runStateOf(c) == SHUTDOWN && ! workQueue.isEmpty()))
return;
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();
} finally {
ctl.set(ctlOf(TERMINATED, 0));
termination.signalAll();
}
return;
}
} finally {
mainLock.unlock();
}
// else retry on failed CAS
}
}
shutdownNow 方法
呼叫 shutdownNow 方法後,執行緒池就不會再接受新的任務了,並且會丟棄工作佇列裡面的任務,正在執行的任務會被中斷,該方法會立刻返回,並不等待啟用的任務執行完成。返回值為這時候佇列裡面被丟棄的任務列表。
public List<Runnable> shutdownNow() {
List<Runnable> tasks;
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
//1. 許可權檢查
checkShutdownAccess();
//2. 設定執行緒池狀態為STOP
advanceRunState(STOP);
//3. 中斷所有執行緒
interruptWorkers();
//4. 將任務佇列移動到tasks中
tasks = drainQueue();
} finally {
mainLock.unlock();
}
tryTerminate();
return tasks;
}
需要注意的是,中斷的所有執行緒包含空閒執行緒和正在執行任務的執行緒。
awaitTermination 方法
當執行緒呼叫 awaitTermination 方法後,當前執行緒會被阻塞,直到執行緒池狀態變為 TERMINATED 才返回,或者等待時間超時才返回。
public boolean awaitTermination(long timeout, TimeUnit unit)
throws InterruptedException {
long nanos = unit.toNanos(timeout);
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
for (;;) {
if (runStateAtLeast(ctl.get(), TERMINATED))
return true;
if (nanos <= 0)
return false;
nanos = termination.awaitNanos(nanos);
}
} finally {
mainLock.unlock();
}
}
首先獲取獨佔鎖,然後在無限迴圈內部判斷當前執行緒池狀態是否至少是 TERMINATED 狀態,如果是則直接返回,否則說明當前執行緒池裡面還有執行緒在執行,則看設定的超時時間 nanos 是否小於 0,小於 0 則說明不需要等待,那就直接返回,如果大於 0 則呼叫條件變數 termination 的 awaitNanos 方法等待 nanos 時間,期望在這段時間內執行緒池狀態變為 TERMINATED。
總結
執行緒池巧妙地使用一個 Integer 型別的原子變數來記錄執行緒池狀態和執行緒池中的執行緒個數。通過執行緒池狀態來控制任務的執行,每個 Worker 執行緒可以處理多個任務。執行緒池通過執行緒的複用減少了執行緒建立和銷燬的開銷。