1. 程式人生 > 其它 >執行緒池中各個引數如何合理設定

執行緒池中各個引數如何合理設定

https://blog.csdn.net/riemann_/article/details/104704197

 

一、前言
在開發過程中,好多場景要用到執行緒池。每次都是自己根據業務場景來設定執行緒池中的各個引數。這兩天又有需求碰到了,索性總結一下方便以後再遇到可以直接看著用。雖說根據業務場景來設定各個引數的值,但有些萬變不離其宗,掌握它的原理對如何用好執行緒池起了至關重要的作用。那我們接下來就來進行執行緒池的分析。

二、ThreadPoolExecutor的重要引數
我們先來看下ThreadPoolExecutor的帶的那些重要引數的構造器。

public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
...
}
1
2
3
4
5
6
7
8
9
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());
1
即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()
1
2
3
4
5
6
7
8
9
10
11
如何來設定呢?

需要根據幾個值來決定

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;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
四、執行緒池佇列的選擇
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中,必須有另一個執行緒正在等待接收這個元素。只有在使用無界執行緒池或者有飽和策略時才建議使用該佇列。

最後我分享一篇用動畫展示執行緒池各個引數的文章:https://zhuanlan.zhihu.com/p/112527671 ,希望對你有幫助。
————————————————
版權宣告:本文為CSDN博主「老周聊架構」的原創文章,遵循CC 4.0 BY-SA版權協議,轉載請附上原文出處連結及本宣告。
原文連結:https://blog.csdn.net/riemann_/article/details/104704197