Java線程池中的各個參數如何合理設置
一、前言
在開發過程中,好多場景要用到線程池。每次都是自己根據業務場景來設置線程池中的各個參數。
這兩天又有需求碰到瞭,索性總結一下方便以後再遇到可以直接看著用。
雖說根據業務場景來設置各個參數的值,但有些萬變不離其宗,掌握它的原理對如何用好線程池起瞭至關重要的作用。
那我們接下來就來進行線程池的分析。
二、ThreadPoolExecutor的重要參數
我們先來看下ThreadPoolExecutor的帶的那些重要參數的構造器。
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) { ... }
1、corePoolSize: 核心線程數
這個應該是最重要的參數瞭,所以如何合理的設置它十分重要。
- 核心線程會一直存活,及時沒有任務需要執行。
- 當線程數小於核心線程數時,即使有線程空閑,線程池也會優先創建新線程處理。
- 設置allowCoreThreadTimeout=true(默認false)時,核心線程會超時關閉。
如何設置好的前提我們要很清楚的知道CPU密集型和IO密集型的區別。
(1)、CPU密集型
CPU密集型也叫計算密集型,指的是系統的硬盤、內存性能相對CPU要好很多,此時,系統運作大部分的狀況是CPU Loading 100%,CPU要讀/寫I/O(硬盤/內存),I/O在很短的時間就可以完成,而CPU還有許多運算要處理,CPU Loading 很高。
在多重程序系統中,大部分時間用來做計算、邏輯判斷等CPU動作的程序稱之CPU bound。例如一個計算圓周率至小數點一千位以下的程序,在執行的過程當中絕大部分時間用在三角函數和開根號的計算,便是屬於CPU bound的程序。
CPU bound的程序一般而言CPU占用率相當高。這可能是因為任務本身不太需要訪問I/O設備,也可能是因為程序是多線程實現因此屏蔽掉瞭等待I/O的時間。
(2)、IO密集型
IO密集型指的是系統的CPU性能相對硬盤、內存要好很多,此時,系統運作,大部分的狀況是CPU在等I/O (硬盤/內存) 的讀/寫操作,此時CPU Loading並不高。
I/O bound的程序一般在達到性能極限時,CPU占用率仍然較低。這可能是因為任務本身需要大量I/O操作,而pipeline做得不是很好,沒有充分利用處理器能力。
好瞭,瞭解完瞭以後我們就開搞瞭。
(3)、先看下機器的CPU核數,然後在設定具體參數:
自己測一下自己機器的核數
System.out.println(Runtime.getRuntime().availableProcessors());
即CPU核數 = Runtime.getRuntime().availableProcessors()
(4)、分析下線程池處理的程序是CPU密集型還是IO密集型
CPU密集型:corePoolSize = CPU核數 + 1
IO密集型:corePoolSize = CPU核數 * 2
2、maximumPoolSize:最大線程數
- 當線程數>=corePoolSize,且任務隊列已滿時。線程池會創建新線程來處理任務。
- 當線程數=maxPoolSize,且任務隊列已滿時,線程池會拒絕處理任務而拋出異常。
3、keepAliveTime:線程空閑時間
- 當線程空閑時間達到keepAliveTime時,線程會退出,直到線程數量=corePoolSize。
- 如果allowCoreThreadTimeout=true,則會直到線程數量=0。
4、queueCapacity:任務隊列容量(阻塞隊列)
- 當核心線程數達到最大時,新任務會放在隊列中排隊等待執行
5、allowCoreThreadTimeout:允許核心線程超時
6、rejectedExecutionHandler:任務拒絕處理器
兩種情況會拒絕處理任務:
- 當線程數已經達到maxPoolSize,且隊列已滿,會拒絕新任務。
- 當線程池被調用shutdown()後,會等待線程池裡的任務執行完畢再shutdown。如果在調用shutdown()和線程池真正shutdown之間提交任務,會拒絕新任務。
線程池會調用rejectedExecutionHandler來處理這個任務。如果沒有設置默認是AbortPolicy,會拋出異常。
ThreadPoolExecutor 采用瞭策略的設計模式來處理拒絕任務的幾種場景。
這幾種策略模式都實現瞭RejectedExecutionHandler 接口。
- AbortPolicy 丟棄任務,拋運行時異常。
- CallerRunsPolicy 執行任務。
- DiscardPolicy 忽視,什麼都不會發生。
- DiscardOldestPolicy 從隊列中踢出最先進入隊列(最後一個執行)的任務。
三、如何設置參數
默認值:
corePoolSize = 1 maxPoolSize = Integer.MAX_VALUE queueCapacity = Integer.MAX_VALUE keepAliveTime = 60s allowCoreThreadTimeout = false rejectedExecutionHandler = AbortPolicy()
如何來設置呢?
需要根據幾個值來決定
tasks :每秒的任務數,假設為500~1000
taskcost:每個任務花費時間,假設為0.1s
responsetime:系統允許容忍的最大響應時間,假設為1s
做幾個計算
corePoolSize = 每秒需要多少個線程處理?
threadcount = tasks/(1/taskcost) = tasks*taskcout = (500 ~ 1000)*0.1 = 50~100 個線程。
corePoolSize設置應該大於50。
根據8020原則,如果80%的每秒任務數小於800,那麼corePoolSize設置為80即可。
queueCapacity = (coreSizePool/taskcost)*responsetime
計算可得 queueCapacity = 80/0.1*1 = 800。意思是隊列裡的線程可以等待1s,超過瞭的需要新開線程來執行。
切記不能設置為Integer.MAX_VALUE,這樣隊列會很大,線程數隻會保持在corePoolSize大小,當任務陡增時,不能新開線程來執行,響應時間會隨之陡增。
maxPoolSize 最大線程數在生產環境上我們往往設置成corePoolSize一樣,這樣可以減少在處理過程中創建線程的開銷。
rejectedExecutionHandler:根據具體情況來決定,任務不重要可丟棄,任務重要則要利用一些緩沖機制來處理。
keepAliveTime和allowCoreThreadTimeout采用默認通常能滿足。
以上都是理想值,實際情況下要根據機器性能來決定。如果在未達到最大線程數的情況機器cpu load已經滿瞭,則需要通過升級硬件和優化代碼,降低taskcost來處理。
以下是我自己的的線程池配置:
@Configuration public class ConcurrentThreadGlobalConfig { @Bean public ThreadPoolTaskExecutor defaultThreadPool() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); //核心線程數目 executor.setCorePoolSize(65); //指定最大線程數 executor.setMaxPoolSize(65); //隊列中最大的數目 executor.setQueueCapacity(650); //線程名稱前綴 executor.setThreadNamePrefix("DefaultThreadPool_"); //rejection-policy:當pool已經達到max size的時候,如何處理新任務 //CALLER_RUNS:不在新線程中執行任務,而是由調用者所在的線程來執行 //對拒絕task的處理策略 executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); //線程空閑後的最大存活時間 executor.setKeepAliveSeconds(60); //加載 executor.initialize(); return executor; } }
四、線程池隊列的選擇
workQueue – 當線程數目超過核心線程數時用於保存任務的隊列。主要有3種類型的BlockingQueue可供選擇:無界隊列,有界隊列和同步移交。從參數中可以看到,此隊列僅保存實現Runnable接口的任務。
這裡再重復一下新任務進入時線程池的執行策略:
- 當正在運行的線程小於corePoolSize,線程池會創建新的線程。
- 當大於corePoolSize而任務隊列未滿時,就會將整個任務塞入隊列。
- 當大於corePoolSize而且任務隊列滿時,並且小於maximumPoolSize時,就會創建新額線程執行任務。
- 當大於maximumPoolSize時,會根據handler策略處理線程。
1、無界隊列
隊列大小無限制,常用的為無界的LinkedBlockingQueue,使用該隊列作為阻塞隊列時要尤其當心,當任務耗時較長時可能會導致大量新任務在隊列中堆積最終導致OOM。
閱讀代碼發現,Executors.newFixedThreadPool 采用就是 LinkedBlockingQueue,而博主踩到的就是這個坑,當QPS很高,發送數據很大,大量的任務被添加到這個無界LinkedBlockingQueue 中,導致cpu和內存飆升服務器掛掉。
當然這種隊列,maximumPoolSize 的值也就無效瞭。
當每個任務完全獨立於其他任務,即任務執行互不影響時,適合於使用無界隊列;例如,在 Web 頁服務器中。
這種排隊可用於處理瞬態突發請求,當命令以超過隊列所能處理的平均數連續到達時,此策略允許無界線程具有增長的可能性。
2、有界隊列
當使用有限的 maximumPoolSizes 時,有界隊列有助於防止資源耗盡,但是可能較難調整和控制。
常用的有兩類,一類是遵循FIFO原則的隊列如ArrayBlockingQueue,另一類是優先級隊列如PriorityBlockingQueue。
PriorityBlockingQueue中的優先級由任務的Comparator決定。
使用有界隊列時隊列大小需和線程池大小互相配合,線程池較小有界隊列較大時可減少內存消耗,降低cpu使用率和上下文切換,但是可能會限制系統吞吐量。
3、同步移交隊列
如果不希望任務在隊列中等待而是希望將任務直接移交給工作線程,可使用SynchronousQueue作為等待隊列。
SynchronousQueue不是一個真正的隊列,而是一種線程之間移交的機制。要將一個元素放入SynchronousQueue中,必須有另一個線程正在等待接收這個元素。
隻有在使用無界線程池或者有飽和策略時才建議使用該隊列。
以上為個人經驗,希望能給大傢一個參考,也希望大傢多多支持WalkonNet。
推薦閱讀:
- 簡單聊一聊Java線程池ThreadPoolExecutor
- 基於ThreadPoolTaskExecutor的使用說明
- Java 線程池全面總結與詳解
- Java線程池必知必會知識點總結
- 淺談為什麼阿裡巴巴要禁用Executors創建線程池