java線程池不同場景下使用示例經(jīng)驗總結(jié)
引導(dǎo)語
ThreadPoolExecutor 初始化時,主要有如下幾個參數(shù):
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) {
大家對這幾個參數(shù)應(yīng)該都很熟悉了,雖然參數(shù)很少,但實際工作中卻有很多門道,大多數(shù)的問題主要集中在線程大小的設(shè)置,隊列大小的設(shè)置兩方面上,接下來我們一起看看工作中,如何初始化 ThreadPoolExecutor。
1、coreSize == maxSize
我相信很多人都看過,或自己寫過這樣的代碼:
ThreadPoolExecutor executor = new ThreadPoolExecutor(10, 10, 600000L, TimeUnit.DAYS, new LinkedBlockingQueue());
這行代碼主要展示了在初始化 ThreadPoolExecutor 的時候,coreSize 和 maxSize 是相等的,這樣設(shè)置的話,隨著請求的不斷增加,會是這樣的現(xiàn)象:
- 請求數(shù) < coreSize 時,新增線程;
- 請求數(shù) >= coreSize && 隊列不滿時,添加任務(wù)入隊;
- 隊列滿時,此時因為 coreSize 和 maxSize 相等,任務(wù)會被直接拒絕。
這么寫的最大目的:是想讓線程一下子增加到 maxSize,并且不要回收線程,防止線程回收,避免不斷增加回收的損耗,一般來說業(yè)務(wù)流量都有波峰低谷,在流量低谷時,線程不會被回收;流量波峰時,maxSize 的線程可以應(yīng)對波峰,不需要慢慢初始化到 maxSize 的過程。
這樣設(shè)置有兩個前提條件:
allowCoreThreadTimeOut 我們采取默認(rèn) false,而不會主動設(shè)置成 true,allowCoreThreadTimeOut 是 false 的話,當(dāng)線程空閑時,就不會回收核心線程;
keepAliveTime 和 TimeUnit 我們都會設(shè)置很大,這樣線程空閑的時間就很長,線程就不會輕易的被回收。
我們現(xiàn)在機(jī)器的資源都是很充足的,我們不用去擔(dān)心線程空閑會浪費(fèi)機(jī)器的資源,所以這種寫法目前是很常見的。
2、maxSize 無界 + SynchronousQueue
在線程池選擇隊列時,我們也會看到有同學(xué)選擇 SynchronousQueue,SynchronousQueue 我們在 《SynchronousQueue 源碼解析》章節(jié)有說過,其內(nèi)部有堆棧和隊列兩種形式,默認(rèn)是堆棧的形式,其內(nèi)部是沒有存儲的容器的,放元素和拿元素是一一對應(yīng)的,比如我使用 put 方法放元素,如果此時沒有對應(yīng)的 take 操作的話,put 操作就會阻塞,需要有線程過來執(zhí)行 take 操作后,put 操作才會返回。
基于此特點(diǎn),如果要使用 SynchronousQueue 的話,我們需要盡量將 maxSize 設(shè)置大一點(diǎn),這樣就可以接受更多的請求。
假設(shè)我們設(shè)置 maxSize 是 10 的話,選擇 SynchronousQueue 隊列,假設(shè)所有請求都執(zhí)行 put 操作,沒有請求執(zhí)行 take 操作,前 10 個 put 請求會消耗 10 個線程,都阻塞在 put 操作上,第 11 個請求過來后,請求就會被拒絕,所以我們才說盡量把 maxSize 設(shè)置大一點(diǎn),防止請求被拒絕。
maxSize 無界 + SynchronousQueue 這樣的組合方式優(yōu)缺點(diǎn)都很明顯:
優(yōu)點(diǎn):
當(dāng)任務(wù)被消費(fèi)時,才會返回,這樣請求就能夠知道當(dāng)前請求是已經(jīng)在被消費(fèi)了,如果是其他的隊列的話,我們只知道任務(wù)已經(jīng)被提交成功了,但無法知道當(dāng)前任務(wù)是在被消費(fèi)中,還是正在隊列中堆積。
缺點(diǎn):
比較消耗資源,大量請求到來時,我們會新建大量的線程來處理請求;
如果請求的量難以預(yù)估的話,maxSize 的大小也很難設(shè)置。
3、maxSize 有界 + Queue 無界
在一些對實時性要求不大,但流量忽高忽低的場景下,可以使用 maxSize 有界 + Queue 無界的組合方式。
比如我們設(shè)置 maxSize 為 20,Queue 選擇默認(rèn)構(gòu)造器的 LinkedBlockingQueue,這樣做的優(yōu)缺點(diǎn)如下:
優(yōu)點(diǎn):
電腦 cpu 固定的情況下,每秒能同時工作的線程數(shù)是有限的,此時開很多的線程其實也是浪費(fèi),還不如把這些請求放到隊列中去等待,這樣可以減少線程之間的 CPU 的競爭;
LinkedBlockingQueue 默認(rèn)構(gòu)造器構(gòu)造出來的鏈表的最大容量是 Integer 的最大值,非常適合流量忽高忽低的場景,當(dāng)流量高峰時,大量的請求被阻塞在隊列中,讓有限的線程可以慢慢消費(fèi)。
缺點(diǎn):
流量高峰時,大量的請求被阻塞在隊列中,對于請求的實時性難以保證,所以當(dāng)對請求的實時性要求較高的場景,不能使用該組合。
4、maxSize 有界 + Queue 有界
這種組合是對 3 缺點(diǎn)的補(bǔ)充,我們把隊列從無界修改成有界,只要排隊的任務(wù)在要求的時間內(nèi),能夠完成任務(wù)即可。
這種組合需要我們把線程和隊列的大小進(jìn)行配合計算,保證大多數(shù)請求都可以在要求的時間內(nèi),有響應(yīng)返回。
5、keepAliveTime 設(shè)置無窮大
有些場景下我們不想讓空閑的線程被回收,于是就把 keepAliveTime 設(shè)置成 0,實際上這種設(shè)置是錯誤的,當(dāng)我們把 keepAliveTime 設(shè)置成 0 時,線程使用 poll 方法在隊列上進(jìn)行超時阻塞時,會立馬返回 null,也就是空閑線程會立馬被回收。
所以如果我們想要空閑的線程不被回收,我們可以設(shè)置 keepAliveTime 為無窮大值,并且設(shè)置 TimeUnit 為時間的大單位,比如我們設(shè)置 keepAliveTime 為 365,TimeUnit 為 TimeUnit.DAYS,意思是線程空閑 1 年內(nèi)都不會被回收。
在實際的工作中,機(jī)器的內(nèi)存一般都夠大,我們合理設(shè)置 maxSize 后,即使線程空閑,我們也不希望線程被回收,我們常常也會設(shè)置 keepAliveTime 為無窮大。
6、線程池的公用和獨(dú)立
在實際工作中,某一個業(yè)務(wù)下的所有場景,我們都不會公用一個線程池,一般有以下幾個原則:
查詢和寫入不公用線程池,互聯(lián)網(wǎng)應(yīng)用一般來說,查詢量遠(yuǎn)遠(yuǎn)大于寫入的量,如果查詢和寫入都要走線程池的話,我們一定不要公用線程池,也就是說查詢走查詢的線程池,寫入走寫入的線程池,如果公用的話,當(dāng)查詢量很大時,寫入的請求可能會到隊列中去排隊,無法及時被處理;
多個寫入業(yè)務(wù)場景看情況是否需要公用線程池,原則上來說,每個業(yè)務(wù)場景都獨(dú)自使用自己的線程池,絕不共用,這樣在業(yè)務(wù)治理、限流、熔斷方面都比較容易,一旦多個業(yè)務(wù)場景公用線程池,可能就會造成業(yè)務(wù)場景之間的互相影響,現(xiàn)在的機(jī)器內(nèi)存都很大,每個寫入業(yè)務(wù)場景獨(dú)立使用自己的線程池也是比較合理的;
多個查詢業(yè)務(wù)場景是可以公用線程池的,查詢的請求一般來說有幾個特點(diǎn):查詢的場景多、rt 時間短、查詢的量比較大,如果給每個查詢場景都弄一個單獨(dú)的線程池的話,第一個比較耗資源,第二個很難定義線程池中線程和隊列的大小,比較復(fù)雜,所以多個相似的查詢業(yè)務(wù)場景是可以公用線程池的。
7、如何算線程大小和隊列大小
在實際的工作中,我們使用線程池時,需要慎重考慮線程的大小和隊列的大小,主要從幾個方面入手:
- 根據(jù)業(yè)務(wù)進(jìn)行考慮,初始化線程池時,我們需要考慮所有業(yè)務(wù)涉及的線程池,如果目前所有的業(yè)務(wù)同時都有很大流量,那么在對于當(dāng)前業(yè)務(wù)設(shè)置線程池時,我們盡量把線程大小、隊列大小都設(shè)置小,如果所有業(yè)務(wù)基本上都不會同時有流量,那么就可以稍微設(shè)置大一點(diǎn);
- 根據(jù)業(yè)務(wù)的實時性要求,如果實時性要求高的話,我們把隊列設(shè)置小一點(diǎn),coreSize == maxSize,并且設(shè)置 maxSize 大一點(diǎn),如果實時性要求低的話,就可以把隊列設(shè)置大一點(diǎn)。
假設(shè)現(xiàn)在機(jī)器上某一時間段只會運(yùn)行一種業(yè)務(wù),業(yè)務(wù)的實時性要求較高,每個請求的平均 rt 是 200ms,請求超時時間是 2000ms,機(jī)器是 4 核 CPU,內(nèi)存 16G,一臺機(jī)器的 qps 是 100,這時候我們可以模擬一下如何設(shè)置:
4 核 CPU,假設(shè) CPU 能夠跑滿,每個請求的 rt 是 200ms,就是 200 ms 能執(zhí)行 4 條請求,2000ms 內(nèi)能執(zhí)行 2000/200 * 4 = 40 條請求;
200 ms 能執(zhí)行 4 條請求,實際上 4 核 CPU 的性能遠(yuǎn)遠(yuǎn)高于這個,我們可以拍腦袋加 10 條,也就是說 2000ms 內(nèi)預(yù)估能夠執(zhí)行 50 條;
一臺機(jī)器的 qps 是 100,此時我們計算一臺機(jī)器 2 秒內(nèi)最多處理 50 條請求,所以此時如果不進(jìn)行 rt 優(yōu)化的話,我們需要加至少一臺機(jī)器。
線程池可以大概這么設(shè)置:
ThreadPoolExecutor executor = new ThreadPoolExecutor(15, 15, 365L, TimeUnit.DAYS, new LinkedBlockingQueue(35));
線程數(shù)最大為 15,隊列最大為 35,這樣機(jī)器差不多可以在 2000ms 內(nèi)處理最大的請求 50 條,當(dāng)然根據(jù)你機(jī)器的性能和實時性要求,你可以調(diào)整線程數(shù)和隊列的大小占比,只要總和小于 50 即可。
以上只是很粗糙的設(shè)置,在實際的工作中,還需要根據(jù)實際情況不斷的觀察和調(diào)整。
8、總結(jié)
線程池設(shè)置非常重要,我們盡量少用 Executors 類提供的各種初始化線程池的方法,多根據(jù)業(yè)務(wù)的量,實時性要求來計算機(jī)器的預(yù)估承載能力,設(shè)置預(yù)估的線程和隊列大小,并且根據(jù)實時請求不斷的調(diào)整線程池的大小值。
以上就是java線程池不同場景下使用示例經(jīng)驗總結(jié)的詳細(xì)內(nèi)容,更多關(guān)于java線程池不同場景使用經(jīng)驗的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
IDEA項目中配置Maven鏡像源(下載源)的詳細(xì)過程
Maven是一個能使我們的java程序開發(fā)節(jié)省時間和精力,是開發(fā)變得相對簡單,還能使開發(fā)規(guī)范化的工具,下面這篇文章主要給大家介紹了關(guān)于IDEA項目中配置Maven鏡像源(下載源)的詳細(xì)過程,需要的朋友可以參考下2024-02-02淺談springboot一個service內(nèi)組件的加載順序
這篇文章主要介紹了springboot一個service內(nèi)組件的加載順序,具有很好的參考價值,希望對大家有所幫助。以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家2021-08-08RocketMQ的push消費(fèi)方式實現(xiàn)示例
這篇文章主要為大家介紹了RocketMQ的push消費(fèi)方式實現(xiàn)示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪<BR>2022-08-08Spring Cloud Alibaba Nacos Config進(jìn)階使用
這篇文章主要介紹了Spring Cloud Alibaba Nacos Config進(jìn)階使用,文中使用企業(yè)案例,圖文并茂的展示了Nacos Config的使用,感興趣的小伙伴可以看一看2021-08-08新的Java訪問mysql數(shù)據(jù)庫工具類的操作代碼
本文通過實例代碼給大家介紹新的Java訪問mysql數(shù)據(jù)庫工具類的方法,本文通過實例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友參考下吧2021-12-12