作者:小傅哥
部落格:https://bugstack.cn
Github:https://github.com/fuzhengwei/CodeGuide/wiki
沉澱、分享、成長,讓自己和他人都能有所收穫!?
一、前言
人看手機,機器學習!
正好是2020年,看到這張圖還是蠻有意思的。以前小時候總會看到一些科技電影,講到機器人會怎樣怎樣,但沒想到人似乎被娛樂化的東西,搞成了低頭族、大肚子!
當意識到這一點時,其實非常懷念小時候。放假的早上跑出去,喊上三五個夥伴,要不下河摸摸魚、彈彈玻璃球、打打pia、跳跳房子!一天下來真的不會感覺累,但現在如果是放假的一天,你的娛樂安排,很多時候會讓頭很累!
就像,你有試過學習一天英語頭疼,還是刷一天抖音頭疼嗎?或者玩一天遊戲與打一天球!如果你意識到了,那麼爭取放下一會手機,適當娛樂,鍛鍊保持個好身體!
二、面試題
謝飛機,小記!
,上次吃虧線上程上,這可能一次坑掉兩次嗎!
謝飛機:你問吧,我準備好了!!!
面試官:嗯,執行緒池狀態是如何設計儲存的?
謝飛機:這!下一個,下一個!
面試官:Worker 的實現類,為什麼不使用 ReentrantLock 來實現呢,而是自己繼承AQS?
謝飛機:我...!
面試官:那你簡述下,execute 的執行過程吧!
謝飛機:再見!
三、執行緒池講解
1. 先看個例子
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10, 10, 0L, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(10));
threadPoolExecutor.execute(() -> {
System.out.println("Hi 執行緒池!");
});
threadPoolExecutor.shutdown();
// Executors.newFixedThreadPool(10);
// Executors.newCachedThreadPool();
// Executors.newScheduledThreadPool(10);
// Executors.newSingleThreadExecutor();
這是一段用於建立執行緒池的例子,相信你已經用了很多次了。
執行緒池的核心目的就是資源的利用,避免重複建立執行緒帶來的資源消耗。因此引入一個池化技術的思想,避免重複建立、銷燬帶來的效能開銷。
那麼,接下來我們就通過實踐的方式分析下這個池子
的構造,看看它是如何處理執行緒
的。
2. 手寫一個執行緒池
2.1 實現流程
為了更好的理解和分析關於執行緒池的原始碼,我們先來按照執行緒池的思想,手寫一個非常簡單的執行緒池。
其實很多時候一段功能程式碼的核心主邏輯可能並沒有多複雜,但為了讓核心流程順利執行,就需要額外新增很多分支的輔助流程。就像我常說的,為了保護手才把擦屁屁紙弄那麼大!
關於圖 21-1,這個手寫執行緒池的實現也非常簡單,只會體現出核心流程,包括:
- 有n個一直在執行的執行緒,相當於我們建立執行緒池時允許的執行緒池大小。
- 把執行緒提交給執行緒池執行。
- 如果執行執行緒池已滿,則把執行緒放入佇列中。
- 最後當有空閒時,則獲取佇列中執行緒進行執行。
2.2 實現程式碼
public class ThreadPoolTrader implements Executor {
private final AtomicInteger ctl = new AtomicInteger(0);
private volatile int corePoolSize;
private volatile int maximumPoolSize;
private final BlockingQueue<Runnable> workQueue;
public ThreadPoolTrader(int corePoolSize, int maximumPoolSize, BlockingQueue<Runnable> workQueue) {
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
}
@Override
public void execute(Runnable command) {
int c = ctl.get();
if (c < corePoolSize) {
if (!addWorker(command)) {
reject();
}
return;
}
if (!workQueue.offer(command)) {
if (!addWorker(command)) {
reject();
}
}
}
private boolean addWorker(Runnable firstTask) {
if (ctl.get() >= maximumPoolSize) return false;
Worker worker = new Worker(firstTask);
worker.thread.start();
ctl.incrementAndGet();
return true;
}
private final class Worker implements Runnable {
final Thread thread;
Runnable firstTask;
public Worker(Runnable firstTask) {
this.thread = new Thread(this);
this.firstTask = firstTask;
}
@Override
public void run() {
Runnable task = firstTask;
try {
while (task != null || (task = getTask()) != null) {
task.run();
if (ctl.get() > maximumPoolSize) {
break;
}
task = null;
}
} finally {
ctl.decrementAndGet();
}
}
private Runnable getTask() {
for (; ; ) {
try {
System.out.println("workQueue.size:" + workQueue.size());
return workQueue.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
private void reject() {
throw new RuntimeException("Error!ctl.count:" + ctl.get() + " workQueue.size:" + workQueue.size());
}
public static void main(String[] args) {
ThreadPoolTrader threadPoolTrader = new ThreadPoolTrader(2, 2, new ArrayBlockingQueue<Runnable>(10));
for (int i = 0; i < 10; i++) {
int finalI = i;
threadPoolTrader.execute(() -> {
try {
Thread.sleep(1500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("任務編號:" + finalI);
});
}
}
}
// 測試結果
任務編號:1
任務編號:0
workQueue.size:8
workQueue.size:8
任務編號:3
workQueue.size:6
任務編號:2
workQueue.size:5
任務編號:5
workQueue.size:4
任務編號:4
workQueue.size:3
任務編號:7
workQueue.size:2
任務編號:6
workQueue.size:1
任務編號:8
任務編號:9
workQueue.size:0
workQueue.size:0
以上,關於執行緒池的實現還是非常簡單的,從測試結果上已經可以把最核心的池化思想體現出來了。主要功能邏輯包括:
ctl
,用於記錄執行緒池中執行緒數量。corePoolSize
、maximumPoolSize
,用於限制執行緒池容量。workQueue
,執行緒池佇列,也就是那些還不能被及時執行的執行緒,會被裝入到這個佇列中。execute
,用於提交執行緒,這個是通用的介面方法。在這個方法裡主要實現的就是,當前提交的執行緒是加入到worker、佇列還是放棄。addWorker
,主要是類Worker
的具體操作,建立並執行執行緒。這裡還包括了getTask()
方法,也就是從佇列中不斷的獲取未被執行的執行緒。
好,那麼以上呢,就是這個簡單執行緒池實現的具體體現。但如果深思熟慮就會發現這裡需要很多完善,比如:執行緒池狀態呢,不可能一直奔跑呀!?
、執行緒池的鎖呢,不會有併發問題嗎?
、執行緒池拒絕後的策略呢?
,這些問題都沒有在主流程解決,也正因為沒有這些流程,所以上面的程式碼才更容易理解。
接下來,我們就開始分析執行緒池的原始碼,與我們實現的簡單執行緒池參考對比,會更加容易理解?!
3. 執行緒池原始碼分析
3.1 執行緒池類關係圖
以圍繞核心類 ThreadPoolExecutor
的實現展開的類之間實現和繼承關係,如圖 21-2 執行緒池類關係圖。
- 介面
Executor
、ExecutorService
,定義執行緒池的基本方法。尤其是execute(Runnable command)
提交執行緒池方法。 - 抽象類
AbstractExecutorService
,實現了基本通用的介面方法。 ThreadPoolExecutor
,是整個執行緒池最核心的工具類方法,所有的其他類和介面,為圍繞這個類來提供各自的功能。Worker
,是任務類,也就是最終執行的執行緒的方法。RejectedExecutionHandler
,是拒絕策略介面,有四個實現類;AbortPolicy(拋異常方式拒絕)
、DiscardPolicy(直接丟棄)
、DiscardOldestPolicy(丟棄存活時間最長的任務)
、CallerRunsPolicy(誰提交誰執行)
。Executors
,是用於建立我們常用的不同策略的執行緒池,newFixedThreadPool
、newCachedThreadPool
、newScheduledThreadPool
、newSingleThreadExecutor
。
3.2 高3位與低29位
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
執行緒池實現類中,使用 AtomicInteger 型別的 ctl 記錄執行緒池狀態和執行緒池數量。在一個型別上記錄多個值,它採用的分割資料區域,高3位
記錄狀態,低29位
儲存執行緒數量,預設 RUNNING 狀態,執行緒數為0個。
3.2 執行緒池狀態
圖 22-4 是執行緒池中的狀態流轉關係,包括如下狀態:
RUNNING
:執行狀態,接受新的任務並且處理佇列中的任務。SHUTDOWN
:關閉狀態(呼叫了shutdown方法)。不接受新任務,,但是要處理佇列中的任務。STOP
:停止狀態(呼叫了shutdownNow方法)。不接受新任務,也不處理佇列中的任務,並且要中斷正在處理的任務。TIDYING
:所有的任務都已終止了,workerCount為0,執行緒池進入該狀態後會調 terminated() 方法進入TERMINATED 狀態。TERMINATED
:終止狀態,terminated() 方法呼叫結束後的狀態。
3.3 提交執行緒(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);
}
在閱讀這部分原始碼的時候,可以參考我們自己實現的執行緒池。其實最終的目的都是一樣的,就是這段被提交的執行緒,啟動執行
、加入佇列
、決策策略
,這三種方式。
ctl.get()
,取的是記錄執行緒狀態和執行緒個數的值,最終需要使用方法workerCountOf()
,來獲取當前執行緒數量。`workerCountOf 執行的是 c & CAPACITY 運算- 根據當前執行緒池中執行緒數量,與核心執行緒數
corePoolSize
做對比,小於則進行新增執行緒到任務執行佇列。 - 如果說此時執行緒數已滿,那麼則需要判斷執行緒池是否為執行狀態
isRunning(c)
。如果是執行狀態則把不能被執行的執行緒放入執行緒佇列中。 - 放入執行緒佇列以後,還需要重新判斷執行緒是否執行以及移除操作,如果非執行且移除,則進行拒絕策略。否則判斷執行緒數量為0後新增新執行緒。
- 最後就是再次嘗試新增任務執行,此時方法 addWorker 的第二個入參是 false,最終會影響新增執行任務數量判斷。如果新增失敗則進行拒絕策略。
3.5 新增執行任務(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 {
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;
新增執行任務的流程可以分為兩塊看,上面程式碼部分是用於記錄執行緒數量、下面程式碼部分是在獨佔鎖裡建立執行執行緒並啟動。這部分程式碼在不看鎖、CAS等操作,那麼就和我們最開始手寫的執行緒池基本一樣了
if (rs >= SHUTDOWN &&! (rs == SHUTDOWN &&firstTask == null &&! workQueue.isEmpty()))
,判斷當前執行緒池狀態,是否為SHUTDOWN
、STOP
、TIDYING
、TERMINATED
中的一個。並且當前狀態為SHUTDOWN
、且傳入的任務為 null,同時佇列不為空。那麼就返回 false。compareAndIncrementWorkerCount
,CAS 操作,增加執行緒數量,成功就會跳出標記的迴圈體。runStateOf(c) != rs
,最後是執行緒池狀態判斷,決定是否迴圈。- 線上程池數量記錄成功後,則需要進入加鎖環節,建立執行執行緒,並記錄狀態。在最後如果判斷沒有啟動成功,則需要執行 addWorkerFailed 方法,剔除到執行緒方法等操作。
3.6 執行執行緒(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();
} finally {
afterExecute(task, thrown);
}
} finally {
task = null;
w.completedTasks++;
w.unlock();
}
}
completedAbruptly = false;
} finally {
processWorkerExit(w, completedAbruptly);
}
}
其實,有了手寫執行緒池的基礎,到這也就基本瞭解了,執行緒池在幹嘛。到這最核心的點就是 task.run()
讓執行緒跑起來。額外再附帶一些其他流程如下;
beforeExecute
、afterExecute
,執行緒執行的前後做一些統計資訊。- 另外這裡的鎖操作是 Worker 繼承 AQS 自己實現的不可重入的獨佔鎖。
processWorkerExit
,如果你感興趣,類似這樣的方法也可以深入瞭解下。線上程退出時候workers做到一些移除處理以及完成任務數等,也非常有意思
3.7 佇列獲取任務(getTask)
如果你已經開始閱讀原始碼,可以在 runWorker 方法中,看到這樣一句迴圈程式碼 while (task != null || (task = getTask()) != null)
。這與我們手寫執行緒池中操作的方式是一樣的,核心目的就是從佇列中獲取執行緒方法。
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.
if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
decrementWorkerCount();
return null;
}
int wc = workerCountOf(c);
// Are workers subject to culling?
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;
}
}
}
- getTask 方法從阻塞佇列中獲取等待被執行的任務,也就是一條條往出拿執行緒方法。
if (rs >= SHUTDOWN ...
,判斷執行緒是否關閉。wc = workerCountOf(c),wc > corePoolSize
,如果工作執行緒數超過核心執行緒數量corePoolSize
並且 workQueue 不為空,則增加工作執行緒。但如果超時未獲取到執行緒,則會把大於 corePoolSize 的執行緒銷燬掉。timed
,是allowCoreThreadTimeOut
得來的。最終timed
為 true 時,則通過阻塞佇列的poll方法進行超時控制。- 如果在
keepAliveTime
時間內沒有獲取到任務,則返回null。如果為false,則阻塞。
四、總結
- 這一章節並沒有完全把執行緒池的所有知識點都介紹完,否則一篇內容會有些臃腫。在這一章節我們從手寫執行緒池開始,逐步的分析這些程式碼在Java的執行緒池中是如何實現的,涉及到的知識點也幾乎是我們以前介紹過的內容,包括:佇列、CAS、AQS、重入鎖、獨佔鎖等內容。所以這些知識也基本是環環相扣的,最好有一些根基否則會有些不好理解。
- 除了本章介紹的,我們還沒有講到執行緒的銷燬過程、四種執行緒池方法的選擇和使用、以及在
CPU密集型任務
、IO 密集型任務
時該怎麼配置。另外在Spring中也有自己實現的執行緒池方法。這些知識點都非常貼近實際操作。 - 好了,今天的內容先扯到這,後續的內容陸續完善。如果以上內容有錯字、流程缺失、或者不好理解以及描述錯誤,歡迎留言。互相學習、互相進步。