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

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


咨詢
建站咨詢
