Java執行緒池核心執行緒用盡後為何優先排隊而不是繼續建立執行緒直至最大執行緒數?

Narcissu5發表於2024-05-02

前陣子在v2ex上看到這篇帖子討論這個問題,有意思的是這個如此基礎的問題在Javaer的世界裡並沒有廣泛的共識,下面的回答也是七嘴八舌的,剛好在《Java Performace》上看到對這個問題的解釋,嘗試總結一下。

原因

書中對執行緒池的解釋基於以下幾點前提:

  1. 如果CPU已經跑滿,增加執行緒並不能提高系統吞吐,更多的執行緒切換開銷反而會降低效能
  2. 核心執行緒用盡之後CPU負載如何執行緒池並不清楚,這取決於核心執行緒數的大小以及當前任務的性質(CPU密集還是IO密集)
  3. 執行緒池不一定要用滿所有CPU,有時執行緒數本來就是一種CPU資源限制的手段

理想情況下執行緒池中Runnable的執行緒數應該剛好等於CPU核心數,如果任務都是CPU密集型,那麼執行緒數就等於核心數;如果是IO密集型,那麼就需要計算CPU耗時和IO耗時的比例來調整核心執行緒數。現實中的情況往往更加複雜,任務可能有CPU密集的也有IO密集的,IO密集的耗時比例也不盡相同。調整核心執行緒數、最大執行緒數和佇列長度來獲得理想的系統吞吐和請求耗時這是開發者的責任,執行緒池提供機制但無法解決這個問題。

執行緒池的邏輯或者說約定:

  1. 假設需要核心執行緒數的執行緒來使CPU達到飽和。如果當前執行緒數沒有達到核心執行緒數,執行緒池總是新建執行緒來執行任務,即使現有執行緒有空閒的。
  2. 如果現有執行緒數達到核心執行緒數而佇列未滿,將任務推進佇列。此時假設CPU資源已經飽和,任務需要等待CPU資源釋放,再增加執行緒只會降低效能。
  3. 如果連佇列都已經滿了,繼續建立執行緒直至最大執行緒數。此時新提交的任務依然排隊,從隊頭取一個任務交給新建立的執行緒。此時執行緒池認為系統已經過載,建立新執行緒屬於試試能不能搶救一下。
  4. 最大執行緒數都達到了,再有新任務提交直接呼叫拒絕策略。

如上,可見設定的關鍵是核心執行緒數,核心執行緒數應該儘量使CPU飽和(或者達到我們期望的負載)但又不會產生過多的上下文切換。考慮到任務的複雜性,這個引數確實只能透過壓力測試來得到。

其它的一些模式

ThreadPoolExecutor的配置是非常靈活的,可能透過調整引數使得執行緒池採取一些別的行為。

令核心執行緒數等於最大執行緒數,就可以取得原貼所期望的,可能也是大部分人所期望的,到達最大執行緒數後再排隊。不過核心執行緒是不會被回收的,如果確實需要回收可以設定allowCoreThreadTimeOut。如果使用無容量限制的佇列如 LinkedBlockedingQueue那麼行為就和Executors#newFixedThreadPool相同。

令佇列長度等於0,最大執行緒數等於無限(Integer#MAX_VALUE,等效於無限),此時所有任務都會直接提交給執行緒,沒有空閒的就新建,不會有任務排隊。此時等效於Executors#newCachedThreadPool

上面兩種方式多多少少都有點問題,這也是為什麼不建議透過Executors來建立執行緒池。

相關文章