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

