新聞中心
作為一名互聯(lián)網(wǎng)程序員,經(jīng)常需要面對(duì)高并發(fā)的場(chǎng)景,為了更好地提高系統(tǒng)的吞吐量和響應(yīng)速度,我們通常采用并發(fā)編程。而線程池技術(shù)也是Java并發(fā)編程中的一個(gè)重要組成部分。本文將分享我的Java線程池使用經(jīng)歷,以及Java線程池在轉(zhuǎn)轉(zhuǎn)平臺(tái)的實(shí)踐。

10多年的棗陽(yáng)網(wǎng)站建設(shè)經(jīng)驗(yàn),針對(duì)設(shè)計(jì)、前端、開(kāi)發(fā)、售后、文案、推廣等六對(duì)一服務(wù),響應(yīng)快,48小時(shí)及時(shí)工作處理。成都全網(wǎng)營(yíng)銷的優(yōu)勢(shì)是能夠根據(jù)用戶設(shè)備顯示端的尺寸不同,自動(dòng)調(diào)整棗陽(yáng)建站的顯示方式,使網(wǎng)站能夠適用不同顯示終端,在瀏覽器中調(diào)整網(wǎng)站的寬度,無(wú)論在任何一種瀏覽器上瀏覽網(wǎng)站,都能展現(xiàn)優(yōu)雅布局與設(shè)計(jì),從而大程度地提升瀏覽體驗(yàn)。成都創(chuàng)新互聯(lián)從事“棗陽(yáng)網(wǎng)站設(shè)計(jì)”,“棗陽(yáng)網(wǎng)站推廣”以來(lái),每個(gè)客戶項(xiàng)目都認(rèn)真落實(shí)執(zhí)行。
一.初識(shí)線程池
線程池是一種常見(jiàn)的多線程并發(fā)編程技術(shù),它將多個(gè)線程組織在一起,以便能夠更有效地管理和控制它們的執(zhí)行。線程池中的每個(gè)線程都可以被重復(fù)利用,避免了頻繁地創(chuàng)建和銷毀線程所帶來(lái)的開(kāi)銷,同時(shí)還可以限制系統(tǒng)中的線程數(shù)量,從而避免了資源的浪費(fèi)和競(jìng)爭(zhēng)。2019年剛參加工作時(shí),我第一次使用線程池是在處理用戶請(qǐng)求,該請(qǐng)求需要聚合多個(gè)服務(wù)的數(shù)據(jù),然后返回給用戶。調(diào)用的服務(wù)均比較耗時(shí),如果串行的去調(diào)用那么系統(tǒng)的響應(yīng)時(shí)間就會(huì)非常長(zhǎng)。所以,我決定使用多線程來(lái)并行執(zhí)行這個(gè)聚合操作,因此也引入了線程池。在Java中線程池是通過(guò)java.util.concurrent包提供的ThreadPoolExecutor類來(lái)實(shí)現(xiàn)的。通過(guò)創(chuàng)建ThreadPoolExecutor對(duì)象并設(shè)置其參數(shù),線程池運(yùn)行大致分為4個(gè)階段大致如下圖:
對(duì)于剛接觸Java線程池的同學(xué),遇到的第一個(gè)問(wèn)題就是如何合理地設(shè)置線程池參數(shù),以最大限度地發(fā)揮線程池的性能,避免線程池滿載或資源浪費(fèi)的問(wèn)題。通過(guò)互聯(lián)網(wǎng)我們能收集到各類設(shè)置線程池參數(shù)的建議:
- corePoolSize:線程池的核心線程數(shù)應(yīng)該根據(jù)應(yīng)用程序的負(fù)載和硬件資源進(jìn)行調(diào)整。一般來(lái)說(shuō),它應(yīng)該設(shè)置為處理當(dāng)前負(fù)載的最大線程數(shù)。如果線程數(shù)太少,可能會(huì)導(dǎo)致請(qǐng)求排隊(duì),降低響應(yīng)速度;如果線程數(shù)太多,可能會(huì)消耗過(guò)多的系統(tǒng)資源。
- maximumPoolSize:最大線程數(shù)應(yīng)該設(shè)置為系統(tǒng)能夠支持的最大線程數(shù),通常不宜過(guò)大。這可以避免系統(tǒng)因線程數(shù)過(guò)多而導(dǎo)致的性能下降和資源浪費(fèi)。
- keepAliveTime:該參數(shù)設(shè)置空閑線程的最長(zhǎng)存活時(shí)間。如果線程池中的線程超過(guò)了corePoolSize,且處于空閑狀態(tài)的時(shí)間超過(guò)了keepAliveTime,這些線程將被終止。這個(gè)時(shí)間需要根據(jù)應(yīng)用程序的負(fù)載和硬件資源進(jìn)行調(diào)整。如果keepAliveTime設(shè)置太短,可能會(huì)導(dǎo)致線程頻繁創(chuàng)建和銷毀,影響性能;如果設(shè)置太長(zhǎng),可能會(huì)消耗過(guò)多的系統(tǒng)資源。
- workQueue:工作隊(duì)列用于存儲(chǔ)等待執(zhí)行的任務(wù)。應(yīng)該根據(jù)應(yīng)用程序的負(fù)載和硬件資源選擇適當(dāng)?shù)年?duì)列類型,比如ArrayBlockingQueue或LinkedBlockingQueue。如果隊(duì)列長(zhǎng)度太小,可能會(huì)導(dǎo)致請(qǐng)求排隊(duì),降低響應(yīng)速度;如果隊(duì)列長(zhǎng)度太大,可能會(huì)消耗過(guò)多的系統(tǒng)資源。
- rejectedExecutionHandler:拒絕策略用于處理當(dāng)工作隊(duì)列已滿,無(wú)法接受新任務(wù)時(shí)的情況??梢赃x擇一些預(yù)定義的策略,比如AbortPolicy、CallerRunsPolicy、DiscardOldestPolicy或DiscardPolicy。需要根據(jù)實(shí)際情況選擇最合適的拒絕策略,以避免任務(wù)丟失或長(zhǎng)時(shí)間阻塞。
總之,合理地設(shè)置線程池的參數(shù)需要程序員對(duì)線程池運(yùn)行原理有足夠的了解,并且有對(duì)應(yīng)用程序的負(fù)載調(diào)優(yōu)和硬件資源調(diào)優(yōu)的經(jīng)驗(yàn),顯然這是非常困難得。因此我最終選擇中庸的配置方法,根據(jù)IO密集型來(lái)設(shè)置線程數(shù)為CPUs*2,根據(jù)平均任務(wù)時(shí)長(zhǎng)與QPS來(lái)預(yù)估隊(duì)列長(zhǎng)度為1000,設(shè)置完畢上線且能夠正常運(yùn)行,就這樣我與線程池的相遇如此簡(jiǎn)單的結(jié)束了。
二.優(yōu)化與實(shí)踐
轉(zhuǎn)眼間來(lái)到2021年,隨著業(yè)務(wù)發(fā)展App使用的人數(shù)越來(lái)越多,對(duì)服務(wù)性能的要求也越來(lái)越高。因此我們?cè)?18前對(duì)服務(wù)進(jìn)行全鏈路壓測(cè),在壓測(cè)中線程池出現(xiàn)以下問(wèn)題:
- 線程池大小不足:線程池大小不足可能導(dǎo)致請(qǐng)求無(wú)法得到處理,進(jìn)而影響系統(tǒng)性能。
- 線程池大小過(guò)大:線程池大小過(guò)大可能會(huì)導(dǎo)致系統(tǒng)資源消耗過(guò)度,影響系統(tǒng)的穩(wěn)定性和性能。
- 隊(duì)列滿了:如果任務(wù)隊(duì)列滿了,新的請(qǐng)求將被拒絕,可能會(huì)導(dǎo)致請(qǐng)求失敗。
- 任務(wù)執(zhí)行時(shí)間過(guò)長(zhǎng):任務(wù)執(zhí)行時(shí)間過(guò)長(zhǎng),影響線程池中其他任務(wù)的執(zhí)行,進(jìn)而影響系統(tǒng)性能。
- 線程池互擾:服務(wù)中存在多個(gè)線程池,其中一個(gè)線程池占用資源過(guò)的,造成其它線程池性能下降。
對(duì)這些問(wèn)題進(jìn)行復(fù)盤(pán)可以發(fā)現(xiàn)在實(shí)際應(yīng)用中,即使是微服務(wù)架構(gòu)的同一個(gè)模塊中由于業(yè)務(wù)的復(fù)雜性也需要引入多個(gè)線程池來(lái)進(jìn)行業(yè)務(wù)隔離,而不同的業(yè)務(wù)場(chǎng)景也需要對(duì)線程池參數(shù)進(jìn)行不同的設(shè)置。比如用戶請(qǐng)求場(chǎng)景需要更大的核心線程數(shù)來(lái)進(jìn)行快速響應(yīng),數(shù)據(jù)導(dǎo)出場(chǎng)景需要更大的隊(duì)列來(lái)緩解大量的導(dǎo)出任務(wù),突發(fā)流量場(chǎng)景需要更大的最大線程數(shù)和任務(wù)隊(duì)列等等。而為了找到合適各場(chǎng)景的參數(shù)值,我們需要重復(fù)進(jìn)行壓測(cè)、調(diào)整參數(shù)、上線的過(guò)程,消耗大量的人力物力。最終我們將遇到的問(wèn)題歸納為兩方面:
- 線程池參數(shù)調(diào)整依賴代碼上線,非常耗時(shí)
- 線程池運(yùn)行情況黑盒,無(wú)法準(zhǔn)確的進(jìn)行調(diào)優(yōu)
為解決這些問(wèn)題我們?cè)O(shè)計(jì)并實(shí)現(xiàn)一套可動(dòng)態(tài)調(diào)整可監(jiān)控的線程池,具體設(shè)計(jì)與實(shí)現(xiàn)如下。
2.1 整體架構(gòu)
動(dòng)態(tài)線程池主要包含客戶端、監(jiān)控平臺(tái)、配置后臺(tái)三部分:
- 客戶端部分是線程池主體部分,動(dòng)態(tài)線程池通過(guò)繼承ThreadPoolExecutor來(lái)實(shí)現(xiàn),保留了Java原生線程池所有的能力,并為業(yè)務(wù)服務(wù)提供線程池創(chuàng)建、注冊(cè)、預(yù)熱和參數(shù)更新的能力。
- 配置后臺(tái)主要負(fù)責(zé)管理線程池配置修改及配置下發(fā),可對(duì)線程池核心參數(shù)corePoolSize、maximumPoolSize、workQueueCapacity進(jìn)行動(dòng)態(tài)修改,無(wú)需業(yè)務(wù)服務(wù)上線。為了能夠在線程池出現(xiàn)異常時(shí)自動(dòng)切換備用參數(shù)方案,我們最終采用配置后臺(tái)為實(shí)現(xiàn)方案。如無(wú)此需求可使用Apollo,Nacos等配置中心實(shí)現(xiàn)成本更小。
- 監(jiān)控報(bào)警平臺(tái)主要負(fù)責(zé)線程池運(yùn)行狀態(tài)的監(jiān)控,可對(duì)線程池的線程池活躍度,隊(duì)列飽和度,隊(duì)列阻塞耗時(shí)進(jìn)行監(jiān)控和報(bào)警。使得程序員能夠?qū)€程池的運(yùn)行情況進(jìn)行直觀的觀察。
2.2 動(dòng)態(tài)參數(shù)實(shí)現(xiàn)
動(dòng)態(tài)參數(shù)調(diào)整主要依賴ThreadPoolExecutor提供的如下的set方法:
public void setCorePoolSize(int corePoolSize);
public void setMaximumPoolSize(int maximumPoolSize);
public void setKeepAliveTime(long time, TimeUnit unit);
public void setThreadFactory(ThreadFactory threadFactory);
public void setRejectedExecutionHandler(RejectedExecutionHandler handler);
綜合考慮需求和風(fēng)險(xiǎn)我們最終選擇使用set方法實(shí)現(xiàn)對(duì)corePoolSize,maximumPoolSize的動(dòng)態(tài)調(diào)整,setCorePoolSize和setMaximumPoolSize方法能夠直接對(duì)當(dāng)前線程池進(jìn)行賦值,并且能夠自動(dòng)調(diào)整線程數(shù)。若當(dāng)前值大于修改值,通過(guò)標(biāo)記中斷的方式回收多余線程。若當(dāng)前值小于修改值,setMaximumPoolSize值進(jìn)行賦值不操作線程,setCorePoolSize會(huì)取排隊(duì)的任務(wù)數(shù)和修改差值的最小值,來(lái)新增對(duì)應(yīng)數(shù)量的核心線程數(shù)??梢钥闯鰏et方法能夠平穩(wěn)的進(jìn)行參數(shù)的修改。這樣解決了線程數(shù)的動(dòng)態(tài)調(diào)整問(wèn)題,但ThreadPoolExecutor不提供對(duì)工作隊(duì)列的動(dòng)態(tài)調(diào)整。重新回顧訴求我們只是想要能夠調(diào)整工作隊(duì)列的大小而不是替換線程池的工作隊(duì)列,因此我們基于LinkedBlockingQueue實(shí)現(xiàn)長(zhǎng)度可調(diào)的工作隊(duì)列。最終實(shí)現(xiàn)效果如下圖:
2.3 線程池監(jiān)控實(shí)現(xiàn)
同樣的線程池監(jiān)控也依賴于ThreadPoolExecutor提供的如下的get方法:
public int getActiveCount();
public BlockingQueuegetQueue;
public int getCorePoolSize();
public int getMaximumPoolSize();
public long getTaskCount();
通過(guò)這些get方法可以實(shí)時(shí)的獲取到線程池的運(yùn)行數(shù)據(jù),將這些數(shù)據(jù)上報(bào)監(jiān)控與報(bào)警平臺(tái)便可讓程序員實(shí)時(shí)查看具體數(shù)據(jù)。具體的實(shí)現(xiàn)方式可以分為兩種:
- 通過(guò)重寫(xiě)ThreadPoolExecutor中的beforeExecute(),afterExecute()方法,在任務(wù)執(zhí)行前后上報(bào)數(shù)據(jù),便可完成監(jiān)控。
- 通過(guò)繼承ThreadPoolExecutor并重載對(duì)應(yīng)的方法增加監(jiān)控代碼,來(lái)進(jìn)行監(jiān)控?cái)?shù)據(jù)數(shù)據(jù)上報(bào)。
對(duì)線程池的監(jiān)控主要是對(duì)工作線程和工作隊(duì)列進(jìn)行監(jiān)控,因此我們整理如下監(jiān)控指標(biāo):
|
指標(biāo) |
方案 |
作用 |
|
線程池活躍度 |
activeCount /maximumPoolSize |
用于描述線程池負(fù)載情況 |
|
隊(duì)列飽和度 |
queueSize / queueCapacity |
用戶描述工作隊(duì)列負(fù)載情況 |
|
任務(wù)阻塞阻塞時(shí)間 |
executeStartTime-inQueueTime |
用戶描述任務(wù)排隊(duì)情況 |
最終監(jiān)控報(bào)警效果:
3.總結(jié)
動(dòng)態(tài)線程池自在轉(zhuǎn)轉(zhuǎn)平臺(tái)應(yīng)用以來(lái),我們通過(guò)日常監(jiān)控及時(shí)發(fā)現(xiàn)潛在問(wèn)題,通過(guò)自動(dòng)容災(zāi)應(yīng)對(duì)突發(fā)流量,通過(guò)壓測(cè)調(diào)優(yōu)提升線程池性能,為轉(zhuǎn)轉(zhuǎn)平臺(tái)服務(wù)在多年的618、雙十一活動(dòng)中保駕護(hù)航,未出現(xiàn)一次因線程池導(dǎo)致的線上事故。希望本文能夠幫助到遇到同樣問(wèn)題的同學(xué)們。
關(guān)于作者
武翱,轉(zhuǎn)轉(zhuǎn)-平臺(tái)技術(shù)部-后端開(kāi)發(fā)。
本文標(biāo)題:動(dòng)態(tài)線程池在轉(zhuǎn)轉(zhuǎn)平臺(tái)的實(shí)踐
當(dāng)前URL:http://m.5511xx.com/article/coccech.html


咨詢
建站咨詢
