再聊執行緒池

OkidoGreen發表於2017-03-27

引言

最近恰好在組內分享執行緒池,又看了看四年前自己寫的執行緒池文章,一是感嘆時光荏苒,二是感嘆當時的理解太淺薄了,三是感嘆自己這麼多年依然停留在淺薄的理解當中,沒有探究其實現,羞愧難當。遂把分享的內容整理出來,希望能夠讓讀者對執行緒池有一個全新的認識。

池化

這裡池化並不是深度學習中的池化,而是將資源交給池來管理的這一過程。我們在開發中經常回接觸到池化資源的技術,最常見的當然是資料庫連線池,以及我們今天要講的執行緒池,那這種池化資源的特點和好處是什麼呢?

特點

  • 通常管理昂貴的資源,如連線、執行緒等
  • 資源的建立和銷燬交給池,呼叫者不需要關心

好處

  • 資源重複利用,提高響應速度
  • 資源可管理,可監控

執行緒池使用

如何使用不再贅述,請看之前的文章執行緒池

執行緒池分析

類結構

這裡寫圖片描述
這裡面的實現類涉及到三個:

  • ForkJoinPool:一個類似於Map/Reduce模型的框架,執行緒級的,詳細可有去看我之前寫的文章Fork/Join-Java平行計算框架
  • ThreadPoolExecutor:這是Java執行緒池的實現,也是本文的主角,Executors提供的幾種執行緒池主要使用該類。
  • ScheduledThreadPoolExecutor:繼承自ThreadPoolExecutor,新增了排程功能。

ThreadPoolExecutor引數

  • int corePoolSize 
    • 執行緒池基本大小
  • int maximumPoolSize 
    • 執行緒池最大大小
  • long keepAliveTime 
    • 保持活動時間
  • TimeUnit unit 
    • 保持活動時間單位
  • BlockingQueue workQueue 
    • 工作佇列
  • ThreadFactory threadFactory 
    • 執行緒工廠
  • RejectedExecutionHandler handler 
    • 駁回回撥

這些引數這樣描述起來很空洞,下面結合執行任務的流程來看一下。

ThreadPoolExecutor執行任務流程

當我們呼叫execute方法時,這個流程就開始了,請看下圖: 
這裡寫圖片描述 
當執行緒池大小 >= corePoolSize 且 佇列未滿時,這時執行緒池使用者與執行緒池之間構成了一個生產者-消費者模型。執行緒池使用者生產任務,執行緒池消費任務,任務儲存在BlockingQueue中,注意這裡入隊使用的是offer,當佇列滿的時候,直接返回false,而不會等待,有關BlockingQueue可以看我之前寫的文章阻塞佇列BlockingQueue。 
這裡寫圖片描述

keepAliveTime

當執行緒處於空閒狀態時,執行緒池需要對它們進行回收,避免浪費資源。但空閒多長時間回收呢,keepAliveTime就是用來設定這個時間的。預設情況下,最終會保留corePoolSize個執行緒避免回收,即使它們是空閒的,以備不時之需。但我們也可以改變這種行為,通過設定allowCoreThreadTimeOut(true)

RejectedExecutionHandler

當佇列滿 且 執行緒池大小 >= maximumPoolSize時會觸發駁回,因為這時執行緒池已經不能響應新提交的任務,駁回時就會回撥這個介面rejectedExecution方法,JDK預設提供了4種駁回策略,程式碼比較簡單,直接上程式碼分析,具體使用何種策略,應該根據業務場景來選擇,執行緒池的預設策略是AbortPolicy。

ThreadPoolExecutor.AbortPolicy

public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
    // 直接丟擲執行時異常
    throw new RejectedExecutionException("Task " + r.toString() +
                                         " rejected from " +
                                         e.toString());
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

ThreadPoolExecutor.CallerRunsPolicy

public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
    if (!e.isShutdown()) {
        // 轉成同步呼叫
        r.run();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

ThreadPoolExecutor.DiscardPolicy

public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
    // 空實現,意味著直接丟棄了
}
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

ThreadPoolExecutor.DiscardOldestPolicy

public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
    if (!e.isShutdown()) {
        // 取出隊首,丟棄
        e.getQueue().poll();
        // 重新提交
        e.execute(r);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

Hook methods

ThreadPoolExecutor預留了以下三個方法,我們可以通過繼承該類來做一些擴充套件,比如監控、日誌等等。

protected void beforeExecute(Thread t, Runnable r) { }
protected void afterExecute(Thread t, Runnable r) { }
protected void terminated() { }
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

ThreadPoolExecutor狀態

執行緒池的工作流程我們應該大致清楚了,其內部同時維護了一個狀態,現在來看一下每種狀態對於任務會造成什麼影響以及狀態之間的流轉。

RUNNING

初始狀態,接受新任務並且處理已經在佇列中的任務。

SHUTDOWN

不接受新任務,但處理佇列中的任務。

STOP

不接受新任務,不處理排隊的任務,並中斷正在進行的任務。

TIDYING

所有任務已終止,workerCount為零,執行緒轉換到狀態TIDYING,這時回撥terminate()方法。

TERMINATED

終態,terminated()執行完成。

這裡寫圖片描述
上圖是這5種狀態間的流轉,可以看到它們是單向的、不可逆的。

擴充套件

  • Tomcat執行緒池
  • Dubbo執行緒池

這兩種執行緒池都是使用ThreadPoolExecutor來實現的,去看它們是如何使用的,有助於我們更好的理解執行緒池。

總結

現在我們在回過頭來去看Executors中提供的幾種執行緒池(fixed、cached、single),如果你能回答出下面幾個問題,說明你明白了執行緒池。

  1. 為什麼newFixedThreadPool中要將corePoolSize和maximumPoolSize設定成一樣?
  2. 為什麼newFixedThreadPool中佇列使用LinkedBlockingQueue?
  3. 為什麼newFixedThreadPool中keepAliveTime會設定成0?
  4. 為什麼newCachedThreadPool中要將corePoolSize設定成0?
  5. 為什麼newCachedThreadPool中佇列使用SynchronousQueue?
  6. 為什麼newSingleThreadExecutor中使用DelegatedExecutorService去包裝ThreadPoolExecutor?

可能到這裡會有人問,講了這麼多,我應該如何去選擇執行緒池?執行緒池應該設定多大?沒有固定的答案,只有適合的答案,下面說一下我的理解:

  • 關於執行緒池大小問題,可以參考這個公式,僅僅是參考而已。 
    • 啟動執行緒數 = [ 任務執行時間 / ( 任務執行時間 - IO等待時間 ) ] x CPU核心數
  • 在控制執行緒池大小的基礎上,儘量使用有界佇列並且設定大小,避免OOM。
  • 設定合理的駁回策略,適用於你的業務。

相關文章