掃二維碼與項目經(jīng)理溝通
我們在微信上24小時期待你的聲音
解答本文疑問/技術咨詢/運營咨詢/技術建議/互聯(lián)網(wǎng)交流
作為一名互聯(lián)網(wǎng)程序員,經(jīng)常需要面對高并發(fā)的場景,為了更好地提高系統(tǒng)的吞吐量和響應速度,我們通常采用并發(fā)編程。而線程池技術也是Java并發(fā)編程中的一個重要組成部分。本文將分享我的Java線程池使用經(jīng)歷,以及Java線程池在轉轉平臺的實踐。

10多年的棗陽網(wǎng)站建設經(jīng)驗,針對設計、前端、開發(fā)、售后、文案、推廣等六對一服務,響應快,48小時及時工作處理。成都全網(wǎng)營銷的優(yōu)勢是能夠根據(jù)用戶設備顯示端的尺寸不同,自動調(diào)整棗陽建站的顯示方式,使網(wǎng)站能夠適用不同顯示終端,在瀏覽器中調(diào)整網(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ù),然后返回給用戶。調(diào)用的服務均比較耗時,如果串行的去調(diào)用那么系統(tǒng)的響應時間就會非常長。所以,我決定使用多線程來并行執(zhí)行這個聚合操作,因此也引入了線程池。在Java中線程池是通過java.util.concurrent包提供的ThreadPoolExecutor類來實現(xiàn)的。通過創(chuàng)建ThreadPoolExecutor對象并設置其參數(shù),線程池運行大致分為4個階段大致如下圖:
對于剛接觸Java線程池的同學,遇到的第一個問題就是如何合理地設置線程池參數(shù),以最大限度地發(fā)揮線程池的性能,避免線程池滿載或資源浪費的問題。通過互聯(lián)網(wǎng)我們能收集到各類設置線程池參數(shù)的建議:
總之,合理地設置線程池的參數(shù)需要程序員對線程池運行原理有足夠的了解,并且有對應用程序的負載調(diào)優(yōu)和硬件資源調(diào)優(yōu)的經(jīng)驗,顯然這是非常困難得。因此我最終選擇中庸的配置方法,根據(jù)IO密集型來設置線程數(shù)為CPUs*2,根據(jù)平均任務時長與QPS來預估隊列長度為1000,設置完畢上線且能夠正常運行,就這樣我與線程池的相遇如此簡單的結束了。
轉眼間來到2021年,隨著業(yè)務發(fā)展App使用的人數(shù)越來越多,對服務性能的要求也越來越高。因此我們在618前對服務進行全鏈路壓測,在壓測中線程池出現(xiàn)以下問題:
對這些問題進行復盤可以發(fā)現(xiàn)在實際應用中,即使是微服務架構的同一個模塊中由于業(yè)務的復雜性也需要引入多個線程池來進行業(yè)務隔離,而不同的業(yè)務場景也需要對線程池參數(shù)進行不同的設置。比如用戶請求場景需要更大的核心線程數(shù)來進行快速響應,數(shù)據(jù)導出場景需要更大的隊列來緩解大量的導出任務,突發(fā)流量場景需要更大的最大線程數(shù)和任務隊列等等。而為了找到合適各場景的參數(shù)值,我們需要重復進行壓測、調(diào)整參數(shù)、上線的過程,消耗大量的人力物力。最終我們將遇到的問題歸納為兩方面:
為解決這些問題我們設計并實現(xiàn)一套可動態(tài)調(diào)整可監(jiān)控的線程池,具體設計與實現(xiàn)如下。
動態(tài)線程池主要包含客戶端、監(jiān)控平臺、配置后臺三部分:
動態(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);
綜合考慮需求和風險我們最終選擇使用set方法實現(xiàn)對corePoolSize,maximumPoolSize的動態(tài)調(diào)整,setCorePoolSize和setMaximumPoolSize方法能夠直接對當前線程池進行賦值,并且能夠自動調(diào)整線程數(shù)。若當前值大于修改值,通過標記中斷的方式回收多余線程。若當前值小于修改值,setMaximumPoolSize值進行賦值不操作線程,setCorePoolSize會取排隊的任務數(shù)和修改差值的最小值,來新增對應數(shù)量的核心線程數(shù)??梢钥闯鰏et方法能夠平穩(wěn)的進行參數(shù)的修改。這樣解決了線程數(shù)的動態(tài)調(diào)整問題,但ThreadPoolExecutor不提供對工作隊列的動態(tài)調(diào)整。重新回顧訴求我們只是想要能夠調(diào)整工作隊列的大小而不是替換線程池的工作隊列,因此我們基于LinkedBlockingQueue實現(xiàn)長度可調(diào)的工作隊列。最終實現(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)方式可以分為兩種:
對線程池的監(jiān)控主要是對工作線程和工作隊列進行監(jiān)控,因此我們整理如下監(jiān)控指標:
|
指標 |
方案 |
作用 |
|
線程池活躍度 |
activeCount /maximumPoolSize |
用于描述線程池負載情況 |
|
隊列飽和度 |
queueSize / queueCapacity |
用戶描述工作隊列負載情況 |
|
任務阻塞阻塞時間 |
executeStartTime-inQueueTime |
用戶描述任務排隊情況 |
最終監(jiān)控報警效果:
動態(tài)線程池自在轉轉平臺應用以來,我們通過日常監(jiān)控及時發(fā)現(xiàn)潛在問題,通過自動容災應對突發(fā)流量,通過壓測調(diào)優(yōu)提升線程池性能,為轉轉平臺服務在多年的618、雙十一活動中保駕護航,未出現(xiàn)一次因線程池導致的線上事故。希望本文能夠幫助到遇到同樣問題的同學們。
武翱,轉轉-平臺技術部-后端開發(fā)。

我們在微信上24小時期待你的聲音
解答本文疑問/技術咨詢/運營咨詢/技術建議/互聯(lián)網(wǎng)交流