Java線程池實現原理深度分析

一、寫在前面

1.1 線程池是什麼

線程池(Thread Pool)是一種基於池化思想管理線程的工具,經常出現在多線程服務器中,如MySQL。

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

而本文描述線程池是JDK中提供的ThreadPoolExecutor類。

當然,使用線程池可以帶來一系列好處:

  • 降低資源消耗:通過池化技術重複利用已創建的線程,降低線程創建和銷燬造成的損耗。
  • 提高響應速度:任務到達時,無需等待線程創建即可立即執行。
  • 提高線程的可管理性:線程是稀缺資源,如果無限制創建,不僅會消耗系統資源,還會因爲線程的不合理分佈導致資源調度失衡,降低系統的穩定性。使用線程池可以進行統一的分配、調優和監控。
  • 提供更多更強大的功能:線程池具備可拓展性,允許開發人員向其中增加更多的功能。比如延時定時線程池ScheduledThreadPoolExecutor,就允許任務延期執行或定期執行。

1.2 線程池解決的問題是什麼

線程池解決的核心問題就是資源管理問題。在併發環境下,系統不能夠確定在任意時刻中,有多少任務需要執行,有多少資源需要投入。這種不確定性將帶來以下若干問題:

  1. 頻繁申請/銷燬資源和調度資源,將帶來額外的消耗,可能會非常巨大。
  2. 對資源無限申請缺少抑制手段,易引發系統資源耗盡的風險。
  3. 系統無法合理管理內部的資源分佈,會降低系統的穩定性。

爲解決資源分配這個問題,線程池採用了“池化”(Pooling)思想。池化,顧名思義,是爲了最大化收益並最小化風險,而將資源統一在一起管理的一種思想。

Pooling is the grouping together of resources (assets, equipment, personnel, effort, etc.) for the purposes of maximizing advantage or minimizing risk to the users. The term is used in finance, computing and equipment management.——wikipedia

“池化”思想不僅僅能應用在計算機領域,在金融、設備、人員管理、工作管理等領域也有相關的應用。

在計算機領域中的表現爲:統一管理IT資源,包括服務器、存儲、和網絡資源等等。通過共享資源,使用戶在低投入中獲益。除去線程池,還有其他比較典型的幾種使用策略包括:

  1. 內存池(Memory Pooling):預先申請內存,提升申請內存速度,減少內存碎片。
  2. 連接池(Connection Pooling):預先申請數據庫連接,提升申請連接的速度,降低系統的開銷。
  3. 實例池(Object Pooling):循環使用對象,減少資源在初始化和釋放時的昂貴損耗。

在瞭解完“是什麼”和“爲什麼”之後,下面我們來一起深入一下線程池的內部實現原理。

 

二、線程池核心設計與實現

在前文中,我們瞭解到:線程池是一種通過“池化”思想,幫助我們管理線程而獲取併發性的工具,在Java中的體現是ThreadPoolExecutor類。那麼它的的詳細設計與實現是什麼樣的呢?我們會在本章進行詳細介紹。

2.1 總體設計

Java中的線程池核心實現類是ThreadPoolExecutor,本章基於JDK 1.8的源碼來分析Java線程池的核心設計與實現。我們首先來看一下ThreadPoolExecutor的UML類圖,瞭解下ThreadPoolExecutor的繼承關係。

ThreadPoolExecutor實現的頂層接口是Executor,頂層接口Executor提供了一種思想:將任務提交和任務執行進行解耦。用戶無需關注如何創建線程,如何調度線程來執行任務,用戶只需提供Runnable對象,將任務的運行邏輯提交到執行器(Executor)中,由Executor框架完成線程的調配和任務的執行部分。ExecutorService接口增加了一些能力:(1)擴充執行任務的能力,補充可以爲一個或一批異步任務生成Future的方法;(2)提供了管控線程池的方法,比如停止線程池的運行。AbstractExecutorService則是上層的抽象類,將執行任務的流程串聯了起來,保證下層的實現只需關注一個執行任務的方法即可。最下層的實現類ThreadPoolExecutor實現最複雜的運行部分,ThreadPoolExecutor將會一方面維護自身的生命週期,另一方面同時管理線程和任務,使兩者良好的結合從而執行並行任務。

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

線程池在內部實際上構建了一個生產者消費者模型,將線程和任務兩者解耦,並不直接關聯,從而良好的緩衝任務,複用線程。線程池的運行主要分成兩部分:任務管理、線程管理。任務管理部分充當生產者的角色,當任務提交後,線程池會判斷該任務後續的流轉:(1)直接申請線程執行該任務;(2)緩衝到隊列中等待線程執行;(3)拒絕該任務。線程管理部分是消費者,它們被統一維護在線程池內,根據任務請求進行線程的分配,當線程執行完任務後則會繼續獲取新的任務去執行,最終當線程獲取不到任務的時候,線程就會被回收。

接下來,我們會按照以下三個部分去詳細講解線程池運行機制:

  1. 線程池如何維護自身狀態。
  2. 線程池如何管理任務。
  3. 線程池如何管理線程。

2.2 生命週期管理

線程池運行的狀態,並不是用戶顯式設置的,而是伴隨着線程池的運行,由內部來維護。線程池內部使用一個變量維護兩個值:運行狀態(runState)和線程數量 (workerCount)。在具體實現中,線程池將運行狀態(runState)、線程數量 (workerCount)兩個關鍵參數的維護放在了一起,如下代碼所示:

private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));

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

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

private static int runStateOf(int c)     { return c & ~CAPACITY; } //計算當前運行狀態
private static int workerCountOf(int c)  { return c & CAPACITY; }  //計算當前線程數量
private static int ctlOf(int rs, int wc) { return rs | wc; }   //通過狀態和線程數生成ctl

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

其生命週期轉換如下入所示:

2.3 任務執行機制

2.3.1 任務調度

任務調度是線程池的主要入口,當用戶提交了一個任務,接下來這個任務將如何執行都是由這個階段決定的。瞭解這部分就相當於瞭解了線程池的核心運行機制。

首先,所有任務的調度都是由execute方法完成的,這部分完成的工作是:檢查現在線程池的運行狀態、運行線程數、運行策略,決定接下來執行的流程,是直接申請線程執行,或是緩衝到隊列中執行,亦或是直接拒絕該任務。其執行過程如下:

  1. 首先檢測線程池運行狀態,如果不是RUNNING,則直接拒絕,線程池要保證在RUNNING的狀態下執行任務。
  2. 如果workerCount < corePoolSize,則創建並啓動一個線程來執行新提交的任務。
  3. 如果workerCount >= corePoolSize,且線程池內的阻塞隊列未滿,則將任務添加到該阻塞隊列中。
  4. 如果workerCount >= corePoolSize && workerCount < maximumPoolSize,且線程池內的阻塞隊列已滿,則創建並啓動一個線程來執行新提交的任務。
  5. 如果workerCount >= maximumPoolSize,並且線程池內的阻塞隊列已滿, 則根據拒絕策略來處理該任務, 默認的處理方式是直接拋異常。

其執行流程如下圖所示:

2.3.2 任務緩衝

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

阻塞隊列(BlockingQueue)是一個支持兩個附加操作的隊列。這兩個附加的操作是:在隊列爲空時,獲取元素的線程會等待隊列變爲非空。當隊列滿時,存儲元素的線程會等待隊列可用。阻塞隊列常用於生產者和消費者的場景,生產者是往隊列裏添加元素的線程,消費者是從隊列裏拿元素的線程。阻塞隊列就是生產者存放元素的容器,而消費者也只從容器裏拿元素。

下圖中展示了線程1往阻塞隊列中添加元素,而線程2從阻塞隊列中移除元素:

 

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

 

2.3.3 任務申請

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

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

獲取任務流程圖

 

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

2.3.4 任務拒絕

任務拒絕模塊是線程池的保護部分,線程池有一個最大的容量,當線程池的任務緩存隊列已滿,並且線程池中的線程數目達到maximumPoolSize時,就需要拒絕掉該任務,採取任務拒絕策略,保護線程池。

拒絕策略是一個接口,其設計如下:

public interface RejectedExecutionHandler {
    void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
}

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

2.4 Worker線程管理

2.4.1 Worker線程

線程池爲了掌握線程的狀態並維護線程的生命週期,設計了線程池內的工作線程Worker。我們來看一下它的部分代碼:

private final class Worker extends AbstractQueuedSynchronizer implements Runnable{
    final Thread thread;//Worker持有的線程
    Runnable firstTask;//初始化的任務,可以爲null
}

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

Worker執行任務的模型如下圖所示:

Worker執行任務

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

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

1.lock方法一旦獲取了獨佔鎖,表示當前線程正在執行任務中。 2.如果正在執行任務,則不應該中斷線程。 3.如果該線程現在不是獨佔鎖的狀態,也就是空閒的狀態,說明它沒有在處理任務,這時可以對該線程進行中斷。 4.線程池在執行shutdown方法或tryTerminate方法時會調用interruptIdleWorkers方法來中斷空閒的線程,interruptIdleWorkers方法會使用tryLock方法來判斷線程池中的線程是否是空閒狀態;如果線程是空閒狀態則可以安全回收。

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

線程池回收過程

 

2.4.2 Worker線程增加

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

申請線程執行流程圖

 

2.4.3 Worker線程回收

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

try {
  while (task != null || (task = getTask()) != null) {
    //執行任務
  }
} finally {
  processWorkerExit(w, completedAbruptly);//獲取不到任務時,主動回收自己
}

線程回收的工作是在processWorkerExit方法完成的。

線程銷燬流程

 

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

2.4.4 Worker線程執行任務

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

1.while循環不斷地通過getTask()方法獲取任務。 2.getTask()方法從阻塞隊列中取任務。 3.如果線程池正在停止,那麼要保證當前線程是中斷狀態,否則要保證當前線程不是中斷狀態。 4.執行任務。 5.如果getTask結果爲null則跳出循環,執行processWorkerExit()方法,銷燬線程。

執行流程如下圖所示:

執行任務流程

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章