歡迎大家關注我的微信公眾號【老周聊架構】,Java后端主流技術堆疊的原理、原始碼分析、架構以及各種互聯網高并發、高性能、高可用的解決方案,
一、前言
上個月底群里的一個好朋友向老周提出啥時候分享 ThreadPoolExecutor 決議大全,我說后面會提上日程;然后前些天有讀者也反饋說在面試中有被問到執行緒池,問我啥時候出一篇執行緒池相關的文章,今天老周就來安排一波執行緒池,現在很多公司都喜歡問執行緒池相關的面試題,為什么面試官這么熱衷于問執行緒池相關的面試題呢?因為這是多執行緒的基礎,ThreadPoolExecutor 的幾個重要引數你必須會知道設定以及什么場景選擇哪種 Executor 、執行緒池佇列的選擇以及相應的拒絕策略,
下面老周收集了幾個朋友提供的大廠關于執行緒池的面試題:
- 執行緒池的使用場景
- 執行緒池各個引數的含義,你平時用的什么佇列以及拒絕策略?
- 程式中哪些地方用到了執行緒池,用執行緒池的好處有哪些?
- 如何自己實作一個執行緒池
- JDK 提供了哪些執行緒池的默認實作
- 阿里巴巴 Java 開發手冊為啥不允許默認實作的執行緒池
- 執行緒池里的引數你是怎么得出來的,根據什么算出來的?
- 說說你自定義執行緒池里的作業流程
- …
這里老周就不帶大家一個個對面試題進行分析了,這里對只講核心原理再結合動態調整執行緒池引數的實踐來幫助你對執行緒池有個清晰的認識,知道了原理再結合自己的實踐,那面試執行緒池也是得心應手了,那你有可能問,老周啊,我平時也沒用到執行緒池啊,用的也都是定義類繼承 Thread類 或者 定義類實作 Runnable 介面來實作多執行緒的啊,額,如果你是面的 Java 中高級開發,那你千萬不要這樣說,這會讓面試官一下覺得你不值中高級,如果你面的中高級還不知道執行緒池的話也沒關系,幸好你看到了老周這篇文章,還不算晚;如果你是已經用過執行緒池相關,那這篇文章也會讓你對執行緒池的原理更加清楚,在專案中應用也會得心應手,
二、執行緒池的概念
2.1 執行緒池是什么
執行緒池是一種執行緒使用模式,執行緒過多會帶來額外的開銷,其中包括創建銷毀執行緒的開銷、調度執行緒的開銷等等,同時也降低了計算機的整體性能,執行緒池維護多個執行緒,等待監督管理者分配可并發執行的任務,這種做法,一方面避免了處理任務時創建銷毀執行緒開銷的代價,另一方面避免了執行緒數量膨脹導致的過分調度問題,保證了對內核的充分利用,
2.2 使用執行緒池的好處
- 降低資源消耗:通過池化技術重復利用已創建的執行緒,降低執行緒創建和銷毀造成的損耗,
- 提高回應速度:任務到達時,無需等待執行緒創建即可立即執行,
- 提高執行緒的可管理性:執行緒是稀缺資源,如果無限制創建,不僅會消耗系統資源,還會因為執行緒的不合理分布導致資源調度失衡,降低系統的穩定性,使用執行緒池可以進行統一的分配、調優和監控,
- 提供更多更強大的功能:執行緒池具備可拓展性,允許開發人員向其中增加更多的功能,比如延時定時執行緒池 ScheduledThreadPoolExecutor,就允許任務延期執行或定期執行,
2.3、ThreadPoolExecutor 的核心引數
網上說的天花亂墜的,也不如直接看 Doug Lea 大佬原始碼的注釋來的更加貼切些,

-
corePoolSize:the number of threads to keep in the pool, even if they are idle, unless {@code allowCoreThreadTimeOut} is set
核心執行緒數:執行緒池中保留的執行緒數,即使它們是空閑的,除非設定 allowCoreThreadTimeOut, -
maximumPoolSize:the maximum number of threads to allow in the pool
最大執行緒數:執行緒池中允許的最大執行緒數 -
keepAliveTime:when the number of threads is greater than the core, this is the maximum time that excess idle threads will wait for new tasks before terminating.
執行緒空閑時間:如果經過 keepAliveTime 時間后,超過核心執行緒數的執行緒還沒有接受到新的任務,那就回收, -
unit:the time unit for the {@code keepAliveTime} argument
單位:keepAliveTime 的時間單位 -
workQueue:the queue to use for holding tasks before they are executed. This queue will hold only the {@code Runnable} tasks submitted by the {@code execute} method.
存放待執行任務的佇列:當提交的任務數超過核心執行緒數后,再提交的任務就存放在這里,它僅僅用來存放被 execute 方法提交的 Runnable 任務,(這里不要再翻譯成作業佇列了好嗎) -
threadFactory:the factory to use when the executor creates a new thread
執行緒工廠:執行程式創建新執行緒時使用的工廠,比如我們專案中自定義的執行緒工廠,排查問題的時候,根據執行緒工廠的名稱就知道這個執行緒來自哪里,很快的定位出問題, -
handler :the handler to use when execution is blocked because the thread bounds and queue capacities are reached
拒絕策略:當佇列里面放滿了任務、最大執行緒數的執行緒都在作業時,這時繼續提交的任務執行緒池就處理不了,應該執行怎么樣的拒絕策略,
三、執行緒池的實作原理
本文描述執行緒池是 JDK 8 中提供的 ThreadPoolExecutor 類,那我們就從 ThreadPoolExecutor 類來看下它的 UML 依賴關系,
3.1 總體設計

- 藍色實線:繼承關系
- 綠色虛線:介面實作關系
- 綠色實作:介面繼承關系
ThreadPoolExecutor 實作的頂層介面是 Executor,頂層介面只提供了void execute(Runnable command); 這么一個方法,Executor 提供的是一種思想:將任務提交和任務執行進行解耦,用戶無需關注如何創建執行緒,如何調度執行緒來執行任務,用戶只需提供 Runnable 物件,將任務的運行邏輯提交到執行器(Executor)中,由 Executor 框架完成執行緒的調配和任務的執行部分,
ExecutorService 介面增加了一些能力:
- 擴充執行任務的能力,補充可以為一個或一批異步任務生成 Future 的方法;
- 提供了管控執行緒池的方法,比如停止執行緒池的運行,
AbstractExecutorService 則是上層的抽象類,將執行任務的流程串聯了起來,保證下層的實作只需關注一個執行任務的方法即可,最下層的實作類 ThreadPoolExecutor 實作最復雜的運行部分,ThreadPoolExecutor 將會一方面維護自身的生命周期,另一方面同時管理執行緒和任務,使兩者良好的結合從而執行并行任務,
我們來看下 ThreadPoolExecutor 的運行流程:

執行緒池在內部實際上構建了一個生產者消費者模型,將執行緒和任務兩者解耦,并不直接關聯,從而良好的緩沖任務,復用執行緒,執行緒池的運行主要分成兩部分:任務管理、執行緒管理,
任務管理部分充當生產者的角色,當任務提交后,執行緒池會判斷該任務后續的流轉:
- 直接申請執行緒執行該任務
- 緩沖到佇列中等待執行緒執行
- 拒絕該任務
執行緒管理部分充當消費者的角色,它們被統一維護在執行緒池內,根據任務請求進行執行緒的分配,當執行緒執行完任務后則會繼續獲取新的任務去執行,最終當執行緒獲取不到任務的時候,執行緒就會被回收,
下面就從以下三個核心機制來詳細講解執行緒池運行機制:
- 執行緒池如何維護自身狀態
- 執行緒池如何管理任務
- 執行緒池如何管理執行緒
3.2 執行緒池如何維護自身狀態
執行緒池運行的狀態,并不是用戶顯式設定的,而是伴隨著執行緒池的運行,由內部來維護,執行緒池內部使用一個變數維護兩個值:運行狀態(runState)和執行緒數量(workerCount),

ctl 這個 AtomicInteger 型別,是對執行緒池的運行狀態和執行緒池中有效執行緒的數量進行控制的一個欄位, 它同時包含兩部分的資訊:執行緒池的運行狀態 (runState) 和執行緒池內有效執行緒的數量 (workerCount),高 3 位保存 runState,低 29 位保存 workerCount,兩個變數之間互不干擾,用一個變數去存盤兩個值,可避免在做相關決策時,出現不一致的情況,不必為了維護兩者的一致,而占用鎖資源,通過閱讀執行緒池源代碼也可以發現,經常出現要同時判斷執行緒池運行狀態和執行緒數量的情況,執行緒池也提供了若干方法去供用戶獲得執行緒池當前的運行狀態、執行緒個數,這里都使用的是位運算的方式,相比于基本運算,速度也會快很多,
關于內部封裝的獲取生命周期狀態、獲取執行緒池執行緒數量的計算方法如下代碼:

哇,Doug Lea 大佬簡直了,設計的真好,老周等等我,這里怎么設計的就好了?CAPACITY 這里是多少呀?
不著急,老周這就帶你來分析分析為什么一個整型變數既可以保存運行狀態,又可以保存執行緒數量?
首先,我們知道 Java 中 1 個整型占 4 個位元組,也就是 32 位,所以 1 個整型有 32 位,
所以整型 1 用二進制表示就是:0000 0000 0000 0000 0000 0000 0000 0001
整型 -1 用二進制表示就是:1111 1111 1111 1111 1111 1111 1111 1111 (這個是補碼,這個忘了的話那得去復習下原碼、反碼、補碼等計算機基礎知識了,)
在 ThreadPoolExecutor,整型中 32 位的前 3 位用來表示執行緒池狀態,后 29 位表示執行緒池中有效的執行緒數,

這里你有可能問了,老周啊,CAPACITY = (1 << 29) - 1 怎么就得到 0001 1111 1111 1111 1111 1111 1111 1111,
好吧,老周就帶你分析下 CAPACITY 怎么來的,下面的那些狀態大家也可以自己去分析下哈,
我們先來看 1 << 29,首先看 1 的二進制代表 0000 0000 0000 0000 0000 0000 0000 0001,
然后 0000 0000 0000 0000 0000 0000 0000 0001 向左移 29 位,得到 0010 0000 0000 0000 0000 0000 0000 0000,
最后將 0010 0000 0000 0000 0000 0000 0000 0000 減 1 得到 0001 1111 1111 1111 1111 1111 1111 1111,
我們下面再來了解下 ThreadPoolExecutor 所定義的狀態,這些狀態都和執行緒的執行密切相關:

- RUNNING:能接受新提交的任務,并且也能處理阻塞佇列中的任務,
- SHUTDOWN:指呼叫了 shutdown() 方法,不再接受新提交的任務,但卻可以繼續處理阻塞佇列中已保存的任務,
- STOP:指呼叫了 shutdownNow() 方法,不再接受新提交的任務,同時拋棄阻塞佇列里的所有任務并中斷所有正在執行任務,
- TIDYING: 所有任務都執行完畢,workerCount 有效執行緒數為 0,
- TERMINATED:終止狀態,當執行 terminated() 后會更新為這個狀態,

3.3 執行緒池如何管理任務
3.3.1 任務調度
任務調度是執行緒池的主要入口,當用戶提交了一個任務,接下來這個任務將如何執行都是由這個階段決定的,了解這部分就相當于了解了執行緒池的核心運行機制,
首先,所有任務的調度都是由 execute 方法完成的,比如我們業務代碼中
threadPool.execute(new Job());,
這部分完成的作業是:檢查現在執行緒池的運行狀態、運行執行緒數、運行策略,決定接下來執行的流程,是直接申請執行緒執行,或是緩沖到佇列中執行,亦或是直接拒絕該任務,其執行程序如下:
- 首先檢測執行緒池運行狀態,如果不是 RUNNING,則直接拒絕,執行緒池要保證在 RUNNING 的狀態下執行任務,
- 如果 workerCount < corePoolSize,則創建并啟動一個執行緒來執行新提交的任務,
- 如果 workerCount >= corePoolSize,且執行緒池內的阻塞佇列未滿,則將任務添加到該阻塞佇列中,
- 如果 workerCount >= corePoolSize && workerCount < maximumPoolSize,且執行緒池內的阻塞佇列已滿,則創建并啟動一個執行緒來執行新提交的任務,
- 如果 workerCount >= maximumPoolSize,并且執行緒池內的阻塞佇列已滿,則根據拒絕策略來處理該任務,默認的處理方式是直接拋例外,
執行流程圖如下:

3.3.2 待執行任務的佇列
待執行任務的佇列是執行緒池能夠管理任務的核心部分,執行緒池的本質是對任務和執行緒的管理,而做到這一點最關鍵的思想就是將任務和執行緒兩者解耦,不讓兩者直接關聯,才可以做后續的分配作業,執行緒池中是以生產者消費者模式,通過一個阻塞佇列來實作的,阻塞佇列快取任務,作業執行緒從阻塞佇列中獲取任務,
阻塞佇列(BlockingQueue)是一個支持兩個附加操作的佇列,
這兩個附加的操作是:
- 在佇列為空時,獲取元素的執行緒會等待佇列變為非空,
- 當佇列滿時,存盤元素的執行緒會等待佇列可用,
阻塞佇列常用于生產者和消費者的場景,生產者是往佇列里添加元素的執行緒,消費者是從佇列里拿元素的執行緒,阻塞佇列就是生產者存放元素的容器,而消費者也只從容器里拿元素,
下圖中展示了 Thread1 往阻塞佇列中添加元素,而執行緒 Thread2 從阻塞佇列中移除元素:

使用不同的佇列可以實作不一樣的任務存取策略,我們下面來看下阻塞佇列的成員:

3.3.3 任務申請
從勺ò干知,任務的執行有兩種可能:
- 一種是任務直接由新創建的執行緒執行
- 另一種是執行緒從任務佇列中獲取任務然后執行,執行完任務的空閑執行緒會再次去從佇列中申請任務再去執行,
第一種情況僅出現在執行緒初始創建的時候,第二種是執行緒獲取任務絕大多數的情況,
執行緒需要從待執行任務的佇列中不斷地取任務執行,幫助執行緒從阻塞佇列中獲取任務,實作執行緒管理模塊和任務管理模塊之間的通信,
這部分策略由 getTask 方法實作,我們來看下 getTask 方法的代碼,

getTask 方法在阻塞佇列中有待執行的任務時會從佇列中彈出一個任務并回傳,如果阻塞佇列為空,那么就會阻塞等待新的任務提交到佇列中直到超時(在一些配置下會一直等待而不超時),如果在超時之前獲取到了新的任務,那么就會將這個任務作為回傳值回傳,所以一般 getTask 方法是不會回傳 null 的,只會阻塞等待下一個任務并在之后將這個新任務作為回傳值回傳,
當 getTask 方法回傳 null 時會導致當前 Worker 退出,當前執行緒被銷毀,在以下情況下 getTask 方法才會回傳 null:
- 當前執行緒池中的執行緒數超過了最大執行緒數,這是因為運行時通過呼叫 setMaximumPoolSize 修改了最大執行緒數而導致的結果;
- 執行緒池處于 STOP 狀態,這種情況下所有執行緒都應該被立即回收銷毀;
- 執行緒池處于 SHUTDOWN 狀態,且阻塞佇列為空,這種情況下已經不會有新的任務被提交到阻塞佇列中了,所以執行緒應該被銷毀;
- 執行緒可以被超時回收的情況下等待新任務超時,執行緒被超時回收一般有以下兩種情況:
- 允許核心執行緒超時(執行緒池配置)的情況下執行緒等待任務超時
- 超出核心執行緒數部分的執行緒等待任務超時
3.3.4 任務拒絕
任務拒絕模塊是執行緒池的保護部分,執行緒池有一個最大的容量,當執行緒池的任務快取佇列已滿,并且執行緒池中的執行緒數目達到 maximumPoolSize 時,就需要拒絕掉該任務,采取任務拒絕策略,保護執行緒池,
拒絕策略是一個介面,其設計如下:

用戶可以通過實作這個介面去定制拒絕策略,也可以選擇 JDK 提供的四種已有拒絕策略,其特點如下:

3.4 執行緒池如何管理執行緒
3.4.1 Worker執行緒
執行緒池為了掌握執行緒的狀態并維護執行緒的生命周期,設計了執行緒池內的作業執行緒 Worker,我們來看一下它的代碼:

Worker 這個作業執行緒,實作了 Runnable 介面,并持有一個執行緒thread,一個初始化的任務firstTask,thread 是在呼叫構造方法時通過 ThreadFactory 來創建的執行緒,可以用來執行任務;
firstTask 用它來保存傳入的第一個任務,這個任務可以有也可以為 null,如果這個值是非空的,那么執行緒就會在啟動初期立即執行這個任務,也就對應核心執行緒創建時的情況;如果這個值是空的,那么就需要創建一個執行緒去執行任務串列(workQueue)中的任務,也就是非核心執行緒的創建,
3.4.1.1 AQS 作用
Worker 繼承了 AbstractQueuedSynchronizer,主要目的有兩個:
-
將鎖的粒度細化到每個 Worker
如果多個 Worker 使用同一個鎖,那么一個 Worker Running 持有鎖的時候,其他 Worker 就無法執行,這顯然是不合理的, -
直接使用 CAS 獲取,避免阻塞,
如果這個鎖使用阻塞獲取,那么在多 Worker 的情況下執行 shutDown,如果這個 Worker 此時正在 Running 無法獲取到鎖,那么執行 shutDown() 執行緒就會阻塞住了,顯然是不合理的,
3.4.1.2 Runnable 作用
Worker 還實作了 Runnable,它有兩個屬性 thead、firstTask,
firstTask 用它來保存傳入的第一個任務,這個任務可以有也可以為 null,
- 如果這個值是非空的,那么執行緒就會在啟動初期立即執行這個任務,也就對應核心執行緒創建時的情況,
- 如果這個值是 null,那么就需要創建一個執行緒去執行任務串列(workQueue)中的任務,也就是非核心執行緒的創建,
根據整體流程:
執行緒池呼叫 execute —> 創建 Worker(設定屬性thead、firstTask)—> worker.thread.start() —> 實際上呼叫的是 worker.run() —> 執行緒池的 runWorker(worker) —> worker.firstTask.run() (如果 firstTask 為 null 就從等待佇列中拉取一個),
Worker 執行任務的模型如下圖所示:

3.4.2 Worker 執行緒增加
增加執行緒是通過執行緒池中的 addWorker 方法,該方法的功能就是增加一個執行緒,該方法不考慮執行緒池是在哪個階段增加的該執行緒,這個分配執行緒的策略是在上個步驟完成的,該步驟僅僅完成增加執行緒,并使它運行,最后回傳是否成功這個結果,
addWorker 方法有兩個引數:firstTask、core,
- firstTask 引數用于指定新增的執行緒執行的第一個任務,該引數可以為空;
- core 引數為 true 表示在新增執行緒時會判斷當前活動執行緒數是否少于 corePoolSize,false 表示新增執行緒前需要判斷當前活動執行緒數是否少于 maximumPoolSize,
我們來看一下 addWorker 的原始碼:

原始碼看著是不是挺費勁的?沒關系,再看一張執行流程圖加深下映象,

3.4.3 Worker 執行緒執行任務
Worker 中的執行緒 start 的時候,呼叫 Worker 本身 run 方法,這個 run 方法呼叫外部類ThreadPoolExecutor 的 runWorker 方法,直接看 runWorker 方法的原始碼:

執行流程如下:
- while 回圈不斷地通過 getTask() 方法獲取任務
- getTask() 方法從阻塞佇列中取任務
- 如果執行緒池正在停止,那么要保證當前執行緒是中斷狀態,否則要保證當前執行緒不是中斷狀態,
- 執行任務
- 如果 getTask 結果為 null 則跳出回圈,執行 processWorkerExit() 方法,銷毀執行緒,

3.4.4 Worker 執行緒回收
執行緒池中執行緒的銷毀依賴 JVM 自動的回收,執行緒池做的作業是根據當前執行緒池的狀態維護一定數量的執行緒參考,防止這部分執行緒被 JVM 回收,當執行緒池決定哪些執行緒需要回收時,只需要將其參考消除即可,Worker 被創建出來后,就會不斷地進行輪詢,然后獲取任務去執行,核心執行緒可以無限等待獲取任務,非核心執行緒要限時獲取任務,當 Worker 無法獲取到任務,也就是獲取的任務為空時,回圈會結束,Worker 會主動消除自身在執行緒池內的參考,
執行緒回收的作業是在 processWorkerExit 方法完成的,

在回收 Worker 的時候執行緒池會嘗試結束自己的運行,tryTerminate 方法:

3.4.4 Worker 執行緒關閉
說到執行緒關閉,我們就不得不來說說 shutdown 方法和 shutdownNow 方法,
3.4.4.1 shutdown

interruptIdleWorkers 方法,注意,這個方法打斷的是閑置 Worker,打斷閑置 Worker 之后,getTask 方法會回傳 null,然后 Worker 會被回收,那什么是閑置 Worker 呢?
閑置 Worker 是這樣解釋的:Worker 運行的時候會去阻塞佇列拿資料(getTask方法),拿的時候如果沒有設定超時時間,那么會一直阻塞等待阻塞佇列進資料,這樣的 Worker 就被稱為閑置 Worker,由于 Worker 也是一個 AQS,在 runWorker 方法里會有一對 lock 和 unlock 操作,這對 lock 操作是為了確保 Worker 不是一個閑置 Worker,
所以 Worker 被設計成一個 AQS 是為了根據 Worker 的鎖來判斷是否是閑置執行緒,是否可以被強制中斷,
下面我們看下 interruptIdleWorkers 方法:

3.4.4.2 shutdownNow
shutdown 方法將執行緒池狀態改成 SHUTDOWN,執行緒池還能繼續處理阻塞佇列里的任務,并且會回收一些閑置的 Worker,但是 shutdownNow 方法不一樣,它會把執行緒池狀態改成 STOP 狀態,這樣不會處理阻塞佇列里的任務,也不會處理新的任務,

shutdownNow 的中斷和 shutdown 方法不一樣,呼叫的是 interruptWorkers 方法:

3.4.4.3 Worker 執行緒關閉小結
shutdown 方法會更新狀態到 SHUTDOWN,不會影響阻塞佇列里任務的執行,但是不會執行新進來的任務,同時也會回收閑置的 Worker,閑置 Worker 的定義上面已經說過了,
shutdownNow 方法會更新狀態到 STOP,會影響阻塞佇列的任務執行,也不會執行新進來的任務,同時會回收所有的 Worker,
?這里老周就不寫總結了,每塊都分析的很清楚了,相信大家看完這篇文章?,心里也有了自己想要的答案,
歡迎大家關注我的公眾號【老周聊架構】,Java后端主流技術堆疊的原理、原始碼分析、架構以及各種互聯網高并發、高性能、高可用的解決方案,

喜歡的話,一鍵三連走一波,
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/278450.html
標籤:其他
上一篇:網路知識點小記
