首頁 > 科技

詳述Java執行緒池實現原理

2021-06-06 10:01:16

一、寫在前面

1.1 執行緒池是什麼

執行緒池(Thread Pool) 是一種池化思想管理執行緒的工具,經常出現在多執行緒伺服器中,如MySQL。

執行緒過多會帶來額外的開銷,其中包括創建銷燬執行緒的開銷,作業系統排程執行緒的開銷等等,同時也降低了計算機的整體效能。執行緒池維護多個執行緒,等待監督管理者分配可併發執行的任務。這種做法,一方面避免了處理任務是創建銷燬執行緒開銷代價,另一方面避免了執行緒數量膨脹導致的過分排程問題,保證了對作業系統核心的充分利用。

本文描述的執行緒池是JDK提供的ThreadPoolExecutor類

使用執行緒池帶來的好處

  • 降低資源消耗:通過赤化技術重複利用已創建的執行緒,降低想成創建和 銷燬造成的消耗

  • 提高響應速度:任務到達時,無需等待執行緒創建即可立即執行

  • 提高執行緒的可管理性:執行緒是稀缺資源,如果無限制創建,不僅會消耗系統資源,還會因為執行緒的不合理分配導致資源排程失衡,降低系統的穩定性。使用執行緒池可以進行統一的分配、調優和監控

  • 提供更多更強大的功能:執行緒池具備可拓展性,允許開發人員向其中增加風多的功能。比如延時定時執行緒池ScheduledThreadPoolExecutor,就允許任務延期執行或定期執行

1.2 執行緒池解決的問題是什麼

執行緒池解決的問題就是資源管理的問題。在併發環境下,系統不能夠確定在任意時刻有多少任務需要執行,有多少資源需要投入。

在這種不確定性下將會帶來以下若干的問題

  • 頻繁申請/銷燬資源和排程資源,將帶來額外的開銷,可能是非常巨大的

  • 對資源無限申請缺少抑制手段,容易引發系統資源耗盡問題的風險

  • 系統無法合理管理內部的資源分佈,會減低系統的穩定性

為了解決資源分配的問題,執行緒池採用「池化」(Pooling)思想。池化,顧名思義,是為了做大化收益並最小化風險,而將資源統一在一起管理的一種思想。

在計算機領域池化技術表現為:統一管理IT資源,包括伺服器資源、儲存、網路資源等。通過共享資源,使使用者在第投入中獲益。

除去執行緒池其他比較典型的幾種使用策略包括

  • 記憶體池(Memory Pooling):預先申請記憶體,提升申請記憶體的速度,減少記憶體碎片

  • 連線池(Connection Pooling):預先申請資料庫連線,提升申請連線的速度,降低系統開銷

  • 例項池(Object Pooling):迴圈使用物件,減資源在初始化和釋放時昂貴的損耗

二、執行緒池和核心設計與實現

2.1 總體設計

Java中執行緒池核心實現類是ThreadPoolExecutor,本章基於JDK1.8的源碼來分析Java執行緒池的核心設計與實現。首先看一下ThreadPoolExecutor的UML圖,瞭解ThreadPoolExecutor的繼承關係

ThreadPoolExecutor實現的頂層介面是Executor,頂層介面Executor提供了一種思想:將任務提交和任務執行進行解耦。使用者無需關注如何創建執行緒,如何排程執行緒來執行任務,使用者只需提供Runnable物件,將任務的運行邏輯提交到執行器Executor中,由Executor框架完成執行緒的調配和任務的執行部分。

ExecutorService

  • 擴充執行任務的能力,補充可以為一個或者一批非同步任務生成Future的方法

  • 提供了管理執行緒池的方法,比如停止執行緒池的運行

AbstractExecutorService

  • 串聯任務流程,保證下層的實現只需要關注一個執行任務的方法

ThreadPoolExecutor

  • 維護自身的生命週期

  • 管理執行緒和任務,使兩者良好的結合從而執行並行任務

ThreadPoolExecutor是如何運行,如何同時維護執行緒和執行任務的呢?其運行機制如下圖所示

ThreadPoolExecutor運行流程

執行緒池在內部實際上構造了一個生產者消費者模型,將執行緒和任務兩者解耦,並不直接關聯,從而良好的管理緩衝任務,複用執行緒。執行緒池的運行主要分成兩部分::任務管理、執行緒管理。任務管理充當生產者角色,當任務提交後,執行緒池會判斷該任務後續流轉

  • 任務申請執行緒執行該任務

  • 緩衝到佇列中等待執行緒執行

  • 拒絕該任務

執行緒管理部分是消費者,它們被統一維護線上程池內,根據任務請求進行執行緒的分配,當執行緒執行完任務後會繼續獲取新的任務執行,最終獲取不到任務的時候,執行緒會被回收。

接下來按照如下三個方面講解執行緒池的運行機制:

  • 執行緒池如何維護自身狀態

  • 執行緒池如何管理任務

  • 執行緒池如何管理執行緒

2.2 生命週期管理

執行緒池運行的狀態,並不是使用者顯式設定的,而是伴隨著執行緒池的運行,由內部來維 護。執行緒池內部使用一個變數維護兩個值:運行狀態 (runState) 和執行緒數量 (workerCount)。在具體實現中,執行緒池將運行狀態 (runState)、執行緒數量 (workerCount)

兩個關鍵參數的維護放在了一起,如下程式碼所示:

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

關於內部封裝的獲取生命週期狀態、獲取執行緒池執行緒數量的計算方法如以下程式碼 所示:

ThreadPoolExecutor 的運行狀態有 5 種,分別為:

其生命週期轉換如下圖所示

執行緒池生命週期

2.3 任務排程機制

2.3.1 任務排程

任務排程是執行緒池的主要入口,當用戶提交了一個任務,接下來這個任務將如何執行 都是由這個階段決定的。瞭解這部分就相當於瞭解了執行緒池的核心運行機制。 首先,所有任務的排程都是由 execute 方法完成的,這部分完成的工作是:檢查現線上程池的運行狀態、運行執行緒數、運行策略,決定接下來執行的流程,是直接申請執行緒執行,或是緩衝到佇列中執行,亦或是直接拒絕該任務。其執行過程如下:

  • 首先檢測執行緒池運行狀態,如果不是 RUNNING,則直接拒絕,執行緒池要保證在 RUNNING 的狀態下執行任務

  • 如果 workerCount < corePoolSize,則創建並啟動一個執行緒來執行新提交的任務

  • 如果 workerCount >= corePoolSize,且執行緒池內的阻塞佇列未滿,則將任務新增到該阻塞佇列中。

  • 如果 workerCount >= corePoolSize && workerCount < maximumPoolSize,且執行緒池內的阻塞佇列已滿,則創建並啟動一個執行緒來執行新提交的任務。

  • 如果 workerCount >= maximumPoolSize,並且執行緒池內的阻塞佇列已滿 , 則根據拒絕策略來處理該任務 , 預設的處理方式是直接拋異常。

其執行流程如下

任務排程流程圖

2.3.2 任務緩衝

2.3.2 任務緩衝

任務緩衝模組是執行緒池能夠管理任務的核心部分。執行緒池的本質是對任務和執行緒的管 理,而做到這一點最關鍵的思想就是將任務和執行緒兩者解耦,不讓兩者直接關聯,才 可以做後續的分配工作。執行緒池中是以生產者消費者模式,通過一個阻塞佇列來實現 的。阻塞佇列快取任務,工作執行緒從阻塞佇列中獲取任務。

阻塞佇列 (BlockingQueue) 是一個支援兩個附加操作的佇列。這兩個附加的操作是: 在佇列為空時,獲取元素的執行緒會等待佇列變為非空。當佇列滿時,儲存元素的執行緒 會等待佇列可用。阻塞佇列常用於生產者和消費者的場景,生產者是往佇列裡新增元 素的執行緒,消費者是從佇列裡拿元素的執行緒。阻塞佇列就是生產者存放元素的容器,而消費者也只從容器裡拿元素。

阻塞佇列

使用不同的佇列可以實現不一樣的任務存取策略。在這裡,我們可以再介紹下阻塞佇列的成員:

2.3.3 任務申請

由上文的任務分配部分可知,任務的執行有兩種可能:一種是任務直接由新創建的線 程執行。另一種是執行緒從任務佇列中獲取任務然後執行,執行完任務的空閒執行緒會再 次去從佇列中申請任務再去執行。第一種情況僅出現線上程初始創建的時候,第二種 是執行緒獲取任務絕大多數的情況。

執行緒需要從任務快取模組中不斷地取任務執行,幫助執行緒從阻塞佇列中獲取任務,實現執行緒管理模組和任務管理模組之間的通訊。這部分策略由 getTask 方法實現,其 執行流程如下圖所示:

執行緒獲取任務的流程

getTask 這部分進行了多次判斷,為的是控制執行緒的數量,使其符合執行緒池的狀 態。如果執行緒池現在不應該持有那麼多執行緒,則會返回 null 值。工作執行緒 Worker 會不斷接收新任務去執行,而當工作執行緒 Worker 接收不到任務的時候,就會開始 被回收。

源碼分析

2.3.4 任務拒絕

任務拒絕模組是執行緒池的保護部分,執行緒池有一個最大的容量,當執行緒池的任務快取 佇列已滿,並且執行緒池中的執行緒數目達到 maximumPoolSize 時,就需要拒絕掉該任務,採取任務拒絕策略,保護執行緒池。

拒絕策略是一個介面,其設計如下:

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

2.4 Worker執行緒管理

2.4.1 Worker執行緒

執行緒池為了掌握執行緒的狀態並維護執行緒的生命週期,設計了執行緒池內的工作執行緒Worker。

Java Worker源碼部分

Worker 這個工作執行緒,實現了 Runnable 介面,並持有一個執行緒 thread,一個初始化的任務 firstTask。thread 是在呼叫構造方法時通過 ThreadFactory 來創建的執行緒,可以用來執行任務;firstTask 用它來儲存傳入的第一個任務,這個任務可以有也可以為 null。如果這個值是非空的,那麼執行緒就會在啟動初期立即執行這個任務,也就對應核心執行緒創建時的情況;如果這個值是 null,那麼就需要創建一個執行緒去執行任務列表(workQueue)中的任務,也就是非核心執行緒的創建。

worker執行任務

執行緒池需要管理執行緒的生命週期,需要線上程長時間不運行的時候進行回收。執行緒池 使用一張 Hash 表去持有執行緒的引用,這樣可以通過新增引用、移除引用這樣的操作 來控制執行緒的生命週期。這個時候重要的就是如何判斷執行緒是否在運行。

Worker 是通過繼承 AQS,使用 AQS 來實現獨佔鎖這個功能。沒有使用可重入鎖ReentrantLock,而是使用 AQS,為的就是實現不可重入的特性去反應執行緒現在的執行狀態。

  • lock方法一旦獲取了獨佔鎖,表示當前執行緒正在執行任務中

  • 如果正在執行任務,則不應該中斷執行緒

  • 如果該執行緒現在不是獨佔鎖狀態,也就是空閒狀態,說明它沒有正在處理任務,這時可以對該執行緒進行中斷

  • 執行緒池在執行shutdown方法或tryTeriminate方法是或呼叫interruptIdleWorkers方法來中斷空閒執行緒,interruptIdleWorkers方法會使用tryLock方法來判斷執行緒池中的執行緒是否是空閒狀態,如果是空閒狀態則可以安全回收

shutdown方法源碼

tryTerminate方法源碼

interruptIdleWorkers方法源碼

線上程回收過程中就使用到了這種特性,回收過程如下圖所示:

執行緒池回收過程

2.4.2 worker執行緒增加

增加執行緒是通過執行緒池中的 addWorker 方法,該方法的功能就是增加一個執行緒, 該方法不考慮執行緒池是在哪個階段增加的該執行緒,這個分配執行緒的策略是在上個步 驟完成的,該步驟僅僅完成增加執行緒,並使它運行,最後返回是否成功這個結果。 addWorker 方法有兩個參數:firstTask、core。firstTask 參數用於指定新增的執行緒執行的第一個任務,該參數可以為空;core 參數為 true 表示在新增執行緒時會判斷當前活動執行緒數是否少於 corePoolSize,false 表示新增執行緒前需要判斷當前活動執行緒數是否少於 maximumPoolSize,其執行流程如下圖所示:

申請執行緒執行流程圖

源碼分析

2.4.3 worker執行緒回收

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

事實上,在這個方法中,將執行緒引用移出執行緒池就已經結束了執行緒銷燬的部分。但由於引起執行緒銷燬的可能性有很多,執行緒池還要判斷是什麼引發了這次銷燬,是否要改變執行緒池的現階段狀態,是否要根據新狀態,重新分配執行緒。

執行緒銷燬流程

2.4.4 worker執行緒執行任務

在 Worker 類中的 run 方法呼叫了 runWorker 方法來執行任務,runWorker 方法的執行過程如下:

  • while迴圈不斷獲取getTask()方法獲取任務

  • getTask()方法從阻塞佇列獲取任務

  • 如果執行緒池正在停止,那麼保證當前執行緒是中斷狀態,否則要保證當前執行緒不是中斷狀態

  • 執行任務

  • 如果getTask結果為null則調出迴圈,執行processWorkerExit(),銷燬執行緒

執行任務流程

2.4.5 worker如何保證核心執行緒不被回收

源碼分析

我們通常都是通過執行execute(Runnable command)方法來向執行緒池提交一個不需要返回結果的任務的如果你需要返回結果那麼就是 <T> Future<T> submit(Callable<T> task)方法)

  • 第一步:execute方法分析

  • 第二步:addWorker()方法分析

  • 第三步:檢視worker中的run()

  • 第四步:檢視runWorker()

我們可以看到beforeExecute(Thread t, Runnable r)方法和afterExecute(Runnable r, Throwable t)會在任務的執行前後執行,我們可以通過繼承執行緒池的方式來重寫這兩個方法,這樣就能夠對任務的執行進行監控啦。

  • 第五步:檢視getTask()

想了解更多精彩內容,快來關注計算機java程式設計


IT145.com E-mail:sddin#qq.com