一文淺析Java中的線程池

學習總結於-《Java併發編程實戰》

爲什麼要使用線程池?

想象一下,如果系統要處理大量簡單的並且處理時間很短的任務的話,每一個任務都去創建一個線程的,雖然這種方式能夠達到我們的目的,但是有沒有更好的方案呢?
要知道創建線程不像創建對象那麼簡單,僅僅是在JVM的堆裏分配一塊內存而已。
創建一個線程,需要調用操作系統內核的API,然後操作系統要爲線程分配一系列的資源,這個成本就很高了,所以線程是一個重量級的對象,應該避免頻繁創建和銷燬

概念區分

線程池的設計不像一般意義上池化資源。一般的池化資源是你需要資源的時候,向池申請資源,用完了就釋放資源。而目前的線程池的設計普遍都是採用 生產者-消費者模式 來設計的。線程池本身是消費者,線程池的使用方式生產者。生產者生產任務,線程池用線程執行任務。

構造器

Java提供的線程池相關的工具類中,最核心的是 ThreadPoolExecutor.
ThreadPoolExecutor的構造函數非常複雜,如下面代碼所示,這個最完備的構造函數有7個參數。

ThreadPoolExecutor(
  int corePoolSize,
  int maximumPoolSize,
  long keepAliveTime,
  TimeUnit unit,
  BlockingQueue<Runnable> workQueue,
  ThreadFactory threadFactory,
  RejectedExecutionHandler handler) 

下面 一 一 介紹這些參數的意義:
如果說線程池是一個項目組的話,那麼線程就是這個項目組裏面的成員。

  • corePoolSize:表示線程池保有的最少線程數。就比如說有的項目組(線程池)很閒,但是也不能把人都調走了,至少得留
    corePoolSize 這麼多人來堅守陣地。
  • maximumPoolSize:表示線程池創建的最大線程數。比如說,當項目組(線程池)很忙時,就需要增派人手,但是也不能無限制的加人,最多就只能加到maximumPoolSize 這麼多人。當項目組忙過了這陣,閒下來時就需要把多的人撤掉,但是最少得留 corePoolSize這麼多人。
  • keepAliveTime & unit:上面提到項目組(線程池)根據忙閒來增減人員,那在程序世界裏,如何定義忙和閒呢?很簡單,一個線程如果在一段時間內,都沒有執行任務,說明很閒,keepAliveTime 和 unit 就是用來定義這個“一段時間”的參數。也就是說,如果一個線程空閒了keepAliveTime & unit這麼久,而且線程池的線程數大於 corePoolSize ,那麼這個空閒的線程就要被回收了。
  • workQueue:工作隊列
  • threadFactory:通過這個參數你可以自定義如何創建線程(Runnable和Callable)
  • handler:通過這個參數你可以自定義任務的拒絕策略。如果線程池中所有的線程都在忙碌,並且工作隊列也滿了(前提是工作隊列是有界隊列),那麼此時提交任務,線程池就會拒絕接收

拒絕策略

對於拒絕的策略,可以通過handler這個參數來指定。ThreadPoolExecutor已經提供了以下4種策略:

  • CallerRunsPolicy:提交任務的線程自己去執行該任務。
  • AbortPolicy:默認的拒絕策略,會throws RejectedExecutionException。
  • DiscardPolicy:直接丟棄任務,沒有任何異常拋出。
  • DiscardOldestPolicy:丟棄最老的任務,其實就是把最早進入工作隊列的任務丟棄,然後把新任務加入到工作隊列。

使用線程池要注意的事項

1. 不建議使用Executors

考慮到 ThreadPoolExecutor 的構造函數實在是有些複雜,所以Java併發包裏提供了一個線程池的靜態工廠類Executors,利用Executors你可以快速創建線程池。不過目前大廠的編碼規範中基本上都不建議使用Executors了,不建議使用Executors的最重要的原因是:Executors提供的很多方法默認使用的都是無界的LinkedBlockingQueue,高負載情境下,無界隊列很容易導致OOM,而OOM會導致所有請求都無法處理,這是致命問題。所以強烈建議使用有界隊列。

2. 如何選擇拒絕策略

使用有界隊列,當任務過多時,線程池會觸發執行拒絕策略,線程池默認的拒絕策略(AbortPolicy)會 throw RejectedExecutionException 這是個運行時異常,對於運行時異常編譯器並不強制catch它,所以開發人員很容易忽略。因此默認拒絕策略要慎重使用。如果線程池處理的任務非常重要,建議自定義自己的拒絕策略;並且在實際工作中,自定義的拒絕策略往往和降級策略配合使用。

3. 注意異常處理

使用線程池,還要注意異常處理的問題,例如通過ThreadPoolExecutor對象的 execute() 方法提交任務時,如果任務在執行的過程中出現運行時異常,會導致執行任務的線程終止;不過,最致命的是任務雖然異常了,但是你卻獲取不到任何通知,這會讓你誤以爲任務都執行得很正常。雖然線程池提供了很多用於異常處理的方法,但是最穩妥和簡單的方案還是捕獲所有異常並按需處理,可以參考下面的示例代碼:

try {
  //業務邏輯
} catch (RuntimeException x) {
  //按需處理
} catch (Throwable x) {
  //按需處理
} 

總結

線程池在Java併發編程領域非常重要,很多大廠的編碼規範都要求必須通過線程池來管理線程。線程池和普通的池化資源有很大不同,線程池實際上是生產者-消費者模式的一種實現,理解生產者-消費者模式是理解線程池的關鍵所在。


技 術 無 他, 唯 有 熟 爾。
知 其 然, 也 知 其 所 以 然。
踏 實 一 些, 不 要 着 急, 你 想 要 的 歲 月 都 會 給 你。


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