執行緒池的基本概念

YangAM發表於2019-01-18

執行緒池,是一種執行緒的使用模式,它為了降低執行緒使用中頻繁的建立和銷燬所帶來的資源消耗與代價。 通過建立一定數量的執行緒,讓他們時刻準備就緒等待新任務的到達,而任務執行結束之後再重新回來繼續待命。

這就是執行緒池最核心的設計思路,「複用執行緒,平攤執行緒的建立與銷燬的開銷代價」。

相比於來一個任務建立一個執行緒的方式,使用執行緒池的優勢體現在如下幾點:

  1. 避免了執行緒的重複建立與開銷帶來的資源消耗代價
  2. 提升了任務響應速度,任務來了直接選一個執行緒執行而無需等待執行緒的建立
  3. 執行緒的統一分配和管理,也方便統一的監控和調優

執行緒池的實現天生就實現了非同步任務介面,允許你提交多個任務到執行緒池,執行緒池負責選用執行緒執行任務排程。

非同步任務在上一篇文章中已經做過一點鋪墊介紹,那麼本篇就在前一篇的基礎上深入的去探討一下非同步任務與執行緒池的相關內容。

基本介紹

在正式介紹執行緒池相關概念之前,我們先看一張執行緒池相關介面的類圖結構,網上盜來的,但畫的還是很全面的。

執行緒池相關類圖

右上角的幾個介面可以先不看,等我們介紹到組合任務的時候會繼續說的,我們看左邊,Executor、ExecutorService 以及 AbstractExecutorService 都是我們熟悉的,它們抽象了任務執行者的基本模型。

ThreadPoolExecutor 是對執行緒池概念的抽象,它天生實現了任務執行的相關介面,也就是說,執行緒池也是一個任務的執行者,允許你向其中提交多個任務,執行緒池將負責分配執行緒與排程任務。

至於 Schedule 執行緒池,它是擴充套件了基礎的執行緒池實現,提供「計劃排程」能力,定時排程任務,延時執行等。

執行緒池基本原理

ThreadPoolExecutor 的建立並不複雜,直接 new 就好,只不過建構函式有好久個過載,我們直接看最底層的那個,也就是引數最多的那個。

public ThreadPoolExecutor
(   int corePoolSize,
    int maximumPoolSize,
    long keepAliveTime,
    TimeUnit unit,
    BlockingQueue<Runnable> workQueue,
    ThreadFactory threadFactory,
    RejectedExecutionHandler handler
)
複製程式碼

建立一個執行緒池需要傳這麼多引數?是不是覺得有點喪心病狂?

不要擔心,我說了,這是最複雜的一個建構函式過載,需要傳入最全面的構造引數。而你日常使用時,當然可以使用 ThreadPoolExecutor 中的其他較為簡便的建構函式,只不過有些你沒傳的引數將配置為預設值而已。

下面我們將從這些引數的含義出發,看看執行緒池 ThreadPoolExecutor 具備一個怎樣的構成結構。

1、執行緒池容量問題

建構函式中有這麼幾個引數是用於配置執行緒池中執行緒容量與生命週期的:

  • corePoolSize
  • maximumPoolSize
  • keepAliveTime

corePoolSize 指定了執行緒池中的核心執行緒的個數,核心執行緒就是永遠不會被銷燬的執行緒,一旦被建立出來就將永遠存活線上程池之中。

maximumPoolSize 指定了執行緒池能夠建立的最大執行緒數量。

keepAliveTime 是用於控制非核心執行緒最長空閒等待時間,如果一個非核心執行緒處理完任務後回到執行緒池待命,超過這個指定時長依然沒有新任務的分配將導致執行緒被銷燬。

2、任務阻塞問題

ThreadPoolExecutor 中有這麼一個欄位:

private final BlockingQueue workQueue;

這個佇列的作用很明顯,就是當執行緒池中的執行緒不夠用的時候,讓任務排隊,等待有執行緒空閒再來取任務去執行。

3、執行緒工廠

執行緒工廠 ThreadFactory 中只定義了一個方法 newThread,子類實現它並按照自己的需求建立一個執行緒返回。

例如 DefaultThreadFactory 實現的該方法將建立一個執行緒,名稱格式: pool-<執行緒池編號>-thread-<執行緒編號>,設定執行緒的優先順序為標準優先順序,非守護執行緒等。

4、任務拒絕策略

建構函式中還有一個引數 handle 是必須傳的,它將為 ThreadPoolExecutor 中的同名欄位賦值。

private volatile RejectedExecutionHandler handler;

RejectedExecutionHandler 中定義了一個 rejectedExecution 用於描述一種任務拒絕策略。那麼哪種情況下才會觸發該方法的呼叫呢?

當執行緒池中的所有執行緒全部分配出去工作了,並且任務阻塞佇列也阻塞滿了,那麼此時新提交的任務將觸發任務拒絕策略

而拒絕策略主要有以下四個子類實現,而它們都是定義在 ThreadPoolExecutor 的內部類,我們看一看都是哪四種策略:

  • AbortPolicy
  • CallerRunsPolicy
  • DiscardOldestPolicy
  • DiscardPolicy

AbortPolicy 是預設的拒絕策略,他的實現就是直接丟擲 RejectedExecutionException 異常。

CallerRunsPolicy 暫停當前提交任務的執行緒返回,自己去執行自己提交過來的任務。

DiscardOldestPolicy 策略將從阻塞任務佇列對頭移除一個任務並將自己排到佇列尾部等待排程執行。

DiscardPolicy 是一種佛系策略,方法體的實現為空,什麼也不做,也即忽略當前任務的提交。

這樣,我們零零散散的對執行緒池的內部有了一個基本的認識,下面我們要把這些都串起來,看一看原始碼。從一個任務的提交,到分配到執行緒執行任務,一整個過程的相關邏輯做一個探究。

看一看原始碼

先來看一看任務的提交方法,submit

submit

之前的文章我們也說過,這個 submit 方法有四個過載,分別允許你傳入不同型別的任務,Runnable 或是 Callable。我們這裡就以前者為例。

這個 RunnableFuture 型別我們之前說過,他只不過是同時繼承了 Runnable 和 Future 介面,象徵性的描述了「這是一個可監控的任務」。

然後你會發現,整個 submit 的核心邏輯在 execute 方法裡面,也就是說 execute 方法才是真正向執行緒池提交任務的方法。我們重點看一看這個 execute 方法。

先看看 ThreadPoolExecutor 中定義幾個重要的欄位:

image

ctl 是一個原子變數,它用了一個 32 位的整型描述了兩個重要資訊。當前執行緒池執行狀態(runState)和當前執行緒池中有效的執行緒個數(workCount)。

runState 佔用高 3 位元位,workCount 佔用低 29 位元位。

接著我們來看 execute 方法的實現:

image

紅框部分:

如果當前執行緒池中的實際工作執行緒數還未達到配置的核心執行緒數量,那麼將呼叫 addWorker 為當前任務建立一個新執行緒並啟動執行。

addWorker 方法程式碼還是有點多的,這裡就截圖出來進行分析了,因為並不難,我們總結下該方法的邏輯:

  1. 死迴圈中判斷執行緒池狀態是否正常,如果不正常被關閉了等,將直接返回 false
  2. 如果正常則 CAS 嘗試為 workerCount 增加一,並建立一個新的執行緒呼叫 start 方法執行任務。

不知道你留意到 addWorker 方法的第二個引數了沒有,這個引數用於指定執行緒池的上界。

如果傳的是 true,則說明使用 corePoolSize 作為上界,也就是此次為任務分配執行緒如果執行緒池中所有的工作執行緒數達到這個 corePoolSize 則將拒絕分配並返回新增失敗。

如果傳的是 false,則使用 maximumPoolSize 作為上界,道理是一樣的。

藍框部分:

從紅框出來,你可以認為任務分配執行緒失敗了,大概率是所有正常工作的執行緒數達到核心執行緒數量了。這部分做的事情就是:

  1. 如果執行緒池狀態正常,就嘗試將當前任務新增到任務阻塞佇列上。
  2. 再一次檢查執行緒池狀態,如果異常了,將撤回剛才新增的任務並根據我們設定的拒絕策略予以拒絕。
  3. 如果發現執行緒池自上次檢查後,所喲執行緒全部死亡,那麼將建立一個空閒執行緒,適當的時候他會去從任務佇列取我們剛剛新增的任務的

黃框部分:

到達黃色部分必然說明執行緒池狀態異常或是佇列新增失敗,大概率是因為佇列滿了無法再新增了。

此時再次呼叫 addWorker 方法,不過這次傳入 false,意思是,我知道所有的核心執行緒都在忙併且任務佇列也排滿了,那麼你就額外建立一個非核心執行緒來執行我的任務吧。

如果失敗了,執行拒絕策略。

我們總結一下任務的提交到分配執行緒,甚至阻塞到任務佇列這一系列過程:

一個任務過來,如果執行緒池中的執行緒數不足我們配置的核心執行緒數,那麼會嘗試建立新執行緒來執行任務,否則會優先把任務往阻塞佇列上新增

如果阻塞佇列上滿員了,那麼說明當前執行緒池中核心執行緒工作量有點大,將開始建立非核心執行緒共同執行任務,直到達到上限或是阻塞佇列不再滿員。

到這裡呢,我們對於任務的提交與執行緒分配已經有了一個基本的認識了,相信你也一定好奇當一個執行緒的任務執行結束之後,他是如何去取下一個任務的。

這部分我們也來分析分析

執行緒池的內部定義了一個 Worker 內部類,這個類有兩個欄位,一個用於儲存當前的任務,一個用於儲存用於執行該任務的執行緒。

addWorker 中會呼叫執行緒的 start 方法,進而會執行 Worker 例項的 run 方法,這個 run 方法是這樣的:

public void run() {
    runWorker(this);
}
複製程式碼

runWorker 很長,就不截出來一點點分析了,我總結下他的實現邏輯:

  1. 如果自己內部的任務是空,則嘗試從阻塞佇列上獲取一個任務
  2. 執行任務
  3. 迴圈的執行 1和2 兩個步驟,直到阻塞佇列中沒有任務可獲取
  4. 呼叫 processWorkerExit 方法移除當前執行緒線上程池中的引用,也就相當於銷燬了一個執行緒,因為不久後會被 GC 回收

但是這裡有一個細節和大家說一下,第一個步驟從任務佇列中取一個任務呼叫的是 getTask 方法。

這個方法設定了一個邏輯,如果執行緒池中正在工作的執行緒數大於設定的核心執行緒數,也就是說執行緒池中存在非核心執行緒,那麼當前執行緒獲取任務時,如果超過指定時長依然沒有獲取,就將返回跳過迴圈執行我們 runWorker 的第四個步驟,移除對該執行緒的引用。

反之,如果此時有效工作執行緒數少於規定的核心執行緒數,則認定當前執行緒是一個核心執行緒,於是對於獲取任務失敗的處理是「阻塞到條件佇列上,等待其他執行緒喚醒」。

什麼時候喚醒也很容易想到了,就是當任務佇列有新任務新增時,會喚醒所有的核心執行緒,他們會去佇列上取任務,沒搶到的依然回去阻塞。

至此,執行緒池相關的內容介紹完畢,有些方法的實現我只是總結了大概的邏輯,具體的尤待你們自己去探究,有問題也歡迎你和我討論。

關注公眾不迷路,一個愛分享的程式設計師。
公眾號回覆「1024」加作者微信一起探討學習!
每篇文章用到的所有案例程式碼素材都會上傳我個人 github
github.com/SingleYam/o…
歡迎來踩!

YangAM 公眾號

相關文章