一文讀懂線程池的實現(xiàn)原理

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

corePoolSize:the number of threads to keep in the pool, even if they are idle, unless {@code allowCoreThreadTimeOut} is set
核心線程數(shù):線程池中保留的線程數(shù),即使它們是空閑的,除非設置 allowCoreThreadTimeOut。maximumPoolSize:the maximum number of threads to allow in the pool
最大線程數(shù):線程池中允許的最大線程數(shù)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.
線程空閑時間:如果經(jīng)過 keepAliveTime 時間后,超過核心線程數(shù)的線程還沒有接受到新的任務,那就回收。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.
存放待執(zhí)行任務的隊列:當提交的任務數(shù)超過核心線程數(shù)后,再提交的任務就存放在這里。它僅僅用來存放被 execute 方法提交的 Runnable 任務。(這里不要再翻譯成工作隊列了好嗎)threadFactory:the factory to use when the executor creates a new thread
線程工廠:執(zhí)行程序創(chuàng)建新線程時使用的工廠。比如我們項目中自定義的線程工廠,排查問題的時候,根據(jù)線程工廠的名稱就知道這個線程來自哪里,很快的定位出問題,handler :the handler to use when execution is blocked because the thread bounds and queue capacities are reached
拒絕策略:當隊列里面放滿了任務、最大線程數(shù)的線程都在工作時,這時繼續(xù)提交的任務線程池就處理不了,應該執(zhí)行怎么樣的拒絕策略。
三、線程池的實現(xiàn)原理
本文描述線程池是 JDK 8 中提供的 ThreadPoolExecutor 類,那我們就從 ThreadPoolExecutor 類來看下它的 UML 依賴關(guān)系。
3.1 總體設計

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

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

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

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

這里你有可能問了,老周啊,CAPACITY = (1 << 29) - 1 怎么就得到 0001 1111 1111 1111 1111 1111 1111 1111。
好吧,老周就帶你分析下 CAPACITY 怎么來的,下面的那些狀態(tài)大家也可以自己去分析下哈。
我們先來看 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 所定義的狀態(tài),這些狀態(tài)都和線程的執(zhí)行密切相關(guān):

RUNNING:能接受新提交的任務,并且也能處理阻塞隊列中的任務。
SHUTDOWN:指調(diào)用了 shutdown() 方法,不再接受新提交的任務,但卻可以繼續(xù)處理阻塞隊列中已保存的任務。
STOP:指調(diào)用了 shutdownNow() 方法,不再接受新提交的任務,同時拋棄阻塞隊列里的所有任務并中斷所有正在執(zhí)行任務。
TIDYING:所有任務都執(zhí)行完畢,workerCount 有效線程數(shù)為 0。
TERMINATED:終止狀態(tài),當執(zhí)行 terminated() 后會更新為這個狀態(tài)。

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

3.3.2 待執(zhí)行任務的隊列
待執(zhí)行任務的隊列是線程池能夠管理任務的核心部分。線程池的本質(zhì)是對任務和線程的管理,而做到這一點最關(guān)鍵的思想就是將任務和線程兩者解耦,不讓兩者直接關(guān)聯(lián),才可以做后續(xù)的分配工作。線程池中是以生產(chǎn)者消費者模式,通過一個阻塞隊列來實現(xiàn)的。阻塞隊列緩存任務,工作線程從阻塞隊列中獲取任務。
阻塞隊列(BlockingQueue)是一個支持兩個附加操作的隊列。
這兩個附加的操作是:
在隊列為空時,獲取元素的線程會等待隊列變?yōu)榉强铡?/span>
當隊列滿時,存儲元素的線程會等待隊列可用。
阻塞隊列常用于生產(chǎn)者和消費者的場景,生產(chǎn)者是往隊列里添加元素的線程,消費者是從隊列里拿元素的線程。阻塞隊列就是生產(chǎn)者存放元素的容器,而消費者也只從容器里拿元素。
下圖中展示了 Thread1 往阻塞隊列中添加元素,而線程 Thread2 從阻塞隊列中移除元素:

使用不同的隊列可以實現(xiàn)不一樣的任務存取策略。我們下面來看下阻塞隊列的成員:

3.3.3 任務申請
從上文可知,任務的執(zhí)行有兩種可能:
一種是任務直接由新創(chuàng)建的線程執(zhí)行
另一種是線程從任務隊列中獲取任務然后執(zhí)行,執(zhí)行完任務的空閑線程會再次去從隊列中申請任務再去執(zhí)行。
第一種情況僅出現(xiàn)在線程初始創(chuàng)建的時候,第二種是線程獲取任務絕大多數(shù)的情況。
線程需要從待執(zhí)行任務的隊列中不斷地取任務執(zhí)行,幫助線程從阻塞隊列中獲取任務,實現(xiàn)線程管理模塊和任務管理模塊之間的通信。
這部分策略由 getTask 方法實現(xiàn),我們來看下 getTask 方法的代碼。

getTask 方法在阻塞隊列中有待執(zhí)行的任務時會從隊列中彈出一個任務并返回,如果阻塞隊列為空,那么就會阻塞等待新的任務提交到隊列中直到超時(在一些配置下會一直等待而不超時),如果在超時之前獲取到了新的任務,那么就會將這個任務作為返回值返回。所以一般 getTask 方法是不會返回 null 的,只會阻塞等待下一個任務并在之后將這個新任務作為返回值返回。
當 getTask 方法返回 null 時會導致當前 Worker 退出,當前線程被銷毀。在以下情況下 getTask 方法才會返回 null:
當前線程池中的線程數(shù)超過了最大線程數(shù)。這是因為運行時通過調(diào)用 setMaximumPoolSize 修改了最大線程數(shù)而導致的結(jié)果;
線程池處于 STOP 狀態(tài)。這種情況下所有線程都應該被立即回收銷毀;
線程池處于 SHUTDOWN 狀態(tài),且阻塞隊列為空。這種情況下已經(jīng)不會有新的任務被提交到阻塞隊列中了,所以線程應該被銷毀;
線程可以被超時回收的情況下等待新任務超時。線程被超時回收一般有以下兩種情況:
允許核心線程超時(線程池配置)的情況下線程等待任務超時
超出核心線程數(shù)部分的線程等待任務超時
3.3.4 任務拒絕
任務拒絕模塊是線程池的保護部分,線程池有一個最大的容量,當線程池的任務緩存隊列已滿,并且線程池中的線程數(shù)目達到 maximumPoolSize 時,就需要拒絕掉該任務,采取任務拒絕策略,保護線程池。
拒絕策略是一個接口,其設計如下:

用戶可以通過實現(xiàn)這個接口去定制拒絕策略,也可以選擇 JDK 提供的四種已有拒絕策略,其特點如下:

3.4 線程池如何管理線程
3.4.1 Worker線程
線程池為了掌握線程的狀態(tài)并維護線程的生命周期,設計了線程池內(nèi)的工作線程 Worker。我們來看一下它的代碼:

Worker 這個工作線程,實現(xiàn)了 Runnable 接口,并持有一個線程thread,一個初始化的任務firstTask。thread 是在調(diào)用構(gòu)造方法時通過 ThreadFactory 來創(chuàng)建的線程,可以用來執(zhí)行任務;
firstTask 用它來保存?zhèn)魅氲牡谝粋€任務,這個任務可以有也可以為 null。如果這個值是非空的,那么線程就會在啟動初期立即執(zhí)行這個任務,也就對應核心線程創(chuàng)建時的情況;如果這個值是空的,那么就需要創(chuàng)建一個線程去執(zhí)行任務列表(workQueue)中的任務,也就是非核心線程的創(chuàng)建。
3.4.1.1 AQS 作用
Worker 繼承了 AbstractQueuedSynchronizer,主要目的有兩個:
將鎖的粒度細化到每個 Worker
如果多個 Worker 使用同一個鎖,那么一個 Worker Running 持有鎖的時候,其他 Worker 就無法執(zhí)行,這顯然是不合理的。直接使用 CAS 獲取,避免阻塞。
如果這個鎖使用阻塞獲取,那么在多 Worker 的情況下執(zhí)行 shutDown。如果這個 Worker 此時正在 Running 無法獲取到鎖,那么執(zhí)行 shutDown() 線程就會阻塞住了,顯然是不合理的。
3.4.1.2 Runnable 作用
Worker 還實現(xiàn)了 Runnable,它有兩個屬性 thead、firstTask。
firstTask 用它來保存?zhèn)魅氲牡谝粋€任務,這個任務可以有也可以為 null。
如果這個值是非空的,那么線程就會在啟動初期立即執(zhí)行這個任務,也就對應核心線程創(chuàng)建時的情況。
如果這個值是 null,那么就需要創(chuàng)建一個線程去執(zhí)行任務列表(workQueue)中的任務,也就是非核心線程的創(chuàng)建。
根據(jù)整體流程:
線程池調(diào)用 execute —> 創(chuàng)建 Worker(設置屬性thead、firstTask)—> worker.thread.start() —> 實際上調(diào)用的是 worker.run() —> 線程池的 runWorker(worker) —> worker.firstTask.run() (如果 firstTask 為 null 就從等待隊列中拉取一個)。
Worker 執(zhí)行任務的模型如下圖所示:

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

源碼看著是不是挺費勁的?沒關(guān)系,再看一張執(zhí)行流程圖加深下印象。

3.4.3 Worker 線程執(zhí)行任務
Worker 中的線程 start 的時候,調(diào)用 Worker 本身 run 方法,這個 run 方法調(diào)用外部類ThreadPoolExecutor 的 runWorker 方法,直接看 runWorker 方法的源碼:

執(zhí)行流程如下:
while 循環(huán)不斷地通過 getTask() 方法獲取任務
getTask() 方法從阻塞隊列中取任務
如果線程池正在停止,那么要保證當前線程是中斷狀態(tài),否則要保證當前線程不是中斷狀態(tài)。
執(zhí)行任務
如果 getTask 結(jié)果為 null 則跳出循環(huán),執(zhí)行 processWorkerExit() 方法,銷毀線程。

3.4.4 Worker 線程回收
線程池中線程的銷毀依賴 JVM 自動的回收,線程池做的工作是根據(jù)當前線程池的狀態(tài)維護一定數(shù)量的線程引用,防止這部分線程被 JVM 回收,當線程池決定哪些線程需要回收時,只需要將其引用消除即可。Worker 被創(chuàng)建出來后,就會不斷地進行輪詢,然后獲取任務去執(zhí)行,核心線程可以無限等待獲取任務,非核心線程要限時獲取任務。當 Worker 無法獲取到任務,也就是獲取的任務為空時,循環(huán)會結(jié)束,Worker 會主動消除自身在線程池內(nèi)的引用。
線程回收的工作是在 processWorkerExit 方法完成的。

在回收 Worker 的時候線程池會嘗試結(jié)束自己的運行,tryTerminate 方法:

3.4.4 Worker 線程關(guān)閉
說到線程關(guān)閉,我們就不得不來說說 shutdown 方法和 shutdownNow 方法。
3.4.4.1 shutdown

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

3.4.4.2 shutdownNow
shutdown 方法將線程池狀態(tài)改成 SHUTDOWN,線程池還能繼續(xù)處理阻塞隊列里的任務,并且會回收一些閑置的 Worker。但是 shutdownNow 方法不一樣,它會把線程池狀態(tài)改成 STOP 狀態(tài),這樣不會處理阻塞隊列里的任務,也不會處理新的任務。

shutdownNow 的中斷和 shutdown 方法不一樣,調(diào)用的是 interruptWorkers 方法:

3.4.4.3 Worker 線程關(guān)閉小結(jié)
shutdown 方法會更新狀態(tài)到 SHUTDOWN,不會影響阻塞隊列里任務的執(zhí)行,但是不會執(zhí)行新進來的任務。同時也會回收閑置的 Worker,閑置 Worker 的定義上面已經(jīng)說過了。
shutdownNow 方法會更新狀態(tài)到 STOP,會影響阻塞隊列的任務執(zhí)行,也不會執(zhí)行新進來的任務。同時會回收所有的 Worker。
這里老周就不寫總結(jié)了,每塊都分析的很清楚了。相信大家看完這篇文章,心里也有了自己想要的答案。
歡迎大家關(guān)注我的公眾號【老周聊架構(gòu)】,Java后端主流技術(shù)棧的原理、源碼分析、架構(gòu)以及各種互聯(lián)網(wǎng)高并發(fā)、高性能、高可用的解決方案。
喜歡的話,點贊、再看、分享三連。

點個在看你最好看
