線程池雖然在并發(fā)編程里很強大唤衫,但線程池使用面臨的核心的問題在于:線程池的參數(shù)并不好配置婆赠。一方面線程池的運行機制不是很好理解,配置合理需要強依賴開發(fā)人員的個人經(jīng)驗和知識佳励;另一方面页藻,線程池執(zhí)行的情況和任務(wù)類型相關(guān)性較大,IO密集型和CPU密集型的任務(wù)運行起來的情況差異非常大植兰,這導致業(yè)界并沒有一些成熟的經(jīng)驗策略幫助開發(fā)人員參考。
美團方案
比如網(wǎng)上流傳的比較多的一個策略:
- 如果是CPU密集型任務(wù)璃吧,就需要盡量壓榨CPU楣导,參考值可以設(shè)為
N(CPU)+1
(比如是4核心 就配置為5) - 如果是IO密集型任務(wù),參考值可以設(shè)置為
2*N(CPU)
CPU密集型的為什么要+1呢畜挨?《Java并發(fā)編程實戰(zhàn)》給出的原因是:即使當計算(CPU)密集型的線程偶爾由于頁缺失故障或者其他原因而暫停時筒繁,這個“額外”的線程也能確保 CPU 的時鐘周期不會被浪費。
這里先來看看美團幫我們總結(jié)的現(xiàn)在業(yè)界的一些線程池調(diào)參方案:
第一套方案是并發(fā)編程實戰(zhàn)給出的巴元,明顯太理論化了毡咏,和實際業(yè)務(wù)想去甚遠!
N(threads) = N(Cpu個數(shù))*U(cpu的使用率)*(1+ 等待時間/計算時間)
第二套方案就沒有考慮多個業(yè)務(wù)線程池的情況逮刨。
第三套方案的用到了TPS來參與計算呕缭,但是這也是流量恒定情況下算出來的,真實情況往往比較隨機。
有啥比較好的辦法嗎恢总?——那就是:線程池參數(shù)動態(tài)化迎罗,采用這種方案最好就是用這么一個辦法來做:
- 簡化線程池配置:線程池構(gòu)造參數(shù)有8個,但是最核心的是3個:corePoolSize片仿、maximumPoolSize纹安,workQueue,它們最大程度地決定了線程池的任務(wù)分配和線程分配策略
- 參數(shù)可動態(tài)修改:為了解決參數(shù)不好配砂豌,修改參數(shù)成本高等問題
- 加線程池監(jiān)控
為什么能做到動態(tài)修改線程池參數(shù)呢厢岂?這是因為JDK本身就提供api方法支持動態(tài)的修改:
至于如何在運行時狀態(tài)實時查看,這里也有一個辦法:用戶基于JDK原生線程池ThreadPoolExecutor提供的幾個public的getter方法阳距,可以讀取到當前線程池的運行狀態(tài)以及參數(shù):
用戶基于這個功能可以了解線程池的實時狀態(tài)塔粒,比如當前有多少個工作線程,執(zhí)行了多少個任務(wù)娄涩,隊列中等待的任務(wù)數(shù)等等窗怒。
Netty進階指南給出來的方案
在Netty服務(wù)編寫的過程中,也要涉及到兩個線程池的參數(shù)配置蓄拣,尤其是IO線程池的配置扬虚,這里書中也給了一套經(jīng)驗方案來針對線程的監(jiān)控情況,可以參考:
同樣的先用CPU核數(shù)*2球恤,看看是否存在瓶頸辜昵,運行時的監(jiān)控則用比較土的辦法了:
- 打印thread dump,同時獲取當時cpu排在前面幾個的線程號
- 然后在線程dump文件中去對應(yīng)的線程號堆棧
- 然后在堆棧中查找是否有SelectotImpl.lookAndDoSelect處的lock信息
如果多次采集都發(fā)現(xiàn)有這堆信息的話咽斧,說明此時此刻的IO線程比較空閑堪置,無需調(diào)整;但是如果一直在read或者write的執(zhí)行處张惹,則說明IO較為繁忙舀锨,可以適當?shù)娜フ{(diào)大NioEventLoop線程的個數(shù)來提升網(wǎng)絡(luò)的讀寫性能。但是這邊線程數(shù)的改動就不是動態(tài)化的了宛逗,服務(wù)啟動后指定的線程數(shù)就不能再修改了坎匿。
參考文章
1、Java線程池實現(xiàn)原理及其在美團業(yè)務(wù)中的實踐
2雷激、微信文章:如何設(shè)置線程池參數(shù)替蔬?美團給出了一個讓面試官虎軀一震的回答。