重新聲明下吃引,雖然開這系列筆記的時候就說了這是最近看javaguide網站,然后為了加深記憶也為了好知識一起分享滓侍,所以把網站中的知識點搬運了一遍顶伞,其中摻雜這我自己的理解和實踐等喝峦。然后各位如果感覺去可以去看原文势誊。附上鏈接:https://snailclimb.gitee.io/javaguide/#/./docs/java/concurrent/java-thread-pool-best-practices
線程池在實際項目中的使用場景
線程池一般用于執(zhí)行多個不相關聯(lián)的耗時任務。沒有多線程的情況下谣蠢,任務使用順序執(zhí)行粟耻,使用了線程池的話可以讓多個不相關聯(lián)的任務同時執(zhí)行。
假設我們要執(zhí)行三個不相關的耗時任務眉踱。使用線程池前后的區(qū)別如下:
注意這里使用多線程執(zhí)行不同的任務挤忙,可以用一個countDownLatch等待子任務執(zhí)行完成才繼續(xù)往下返回結果。
線程池最佳實踐
使用ThreadPoolExecutor的構造函數(shù)聲明線程池
線程池必須手動通過ThreadPoolExecutor的構造函數(shù)聲明谈喳,避免使用Executors類的newFixedThreadPool和newCachedThreadPool册烈,因為可能會有oom的風險。
說白了就是使用有界隊列婿禽,控制線程創(chuàng)建數(shù)量茄厘。
除了避免OOM的原因外,不推薦使用Executors提佛那個的快捷線程池還有兩個原因:
- 實際使用中需要根據(jù)自己機器的性能谈宛,業(yè)務場景來手動配置線程池的參數(shù)。比如核心線程數(shù)胎署,最大線程數(shù)吆录,使用的任務隊列,拒絕策略等琼牧。
- 我們應該顯示的給我們的線程池命名恢筝,這樣有助于我們定位問題。
監(jiān)測線程池運行狀態(tài)
我們可以通過一些手段來檢測線程池的運行狀態(tài)巨坊,比如SpringBoot中的Actuator組件撬槽。
除此之外我們還可以通過ThreadPoolExecutor的相關api做一個簡陋的監(jiān)控。從下圖可以看出趾撵,ThreadPoolExecutor提供了互毆線程池當前的線程數(shù)和活躍線程數(shù)侄柔,已執(zhí)行完成的任務數(shù),正在排隊的任務數(shù)等任務占调。
建議不同類別的業(yè)務用不同的線程池
很多人在實際項目中會有類似這樣的問題:我的項目中多個業(yè)務需要用到線程池暂题,是為每個線程池都定義一個還是說定義一個公共的線程池呢?
一般建議不同的業(yè)務使用不同的線程池究珊, 配置線程池的時候根據(jù)當前業(yè)務情況對當前線程池進行配置薪者,因為不同的業(yè)務的并發(fā)以及對資源的使用情況都不同,重新優(yōu)化系統(tǒng)性能瓶頸相關的業(yè)務剿涮。
下面我們看一個線程池運用不當?shù)木€上事故案例:
圖中的代碼可能會存在死鎖的情況言津。為什么呢攻人?下面我們捋一捋。
試想這么一個極端現(xiàn)象:如果核心線程數(shù)是n悬槽,父任務數(shù)量也為n怀吻。把核心線程全部占用。然后父任務下的子任務也需要用線程陷谱。在任務隊列中阻塞等待獲取線程烙博。而父任務在等待子任務執(zhí)行完成,子任務等待父任務釋放線程資源好獲取線程烟逊。也就造成了死鎖渣窜。
解決方法也很簡單,新增加一個用于執(zhí)行子任務的線程池專門為其服務宪躯。
要給線程池命名
初始化線程池的時候需要顯示命名(設置線程池名稱前綴)乔宿,有利于定位問題。
默認情況下創(chuàng)建的線程名字類似pool-1-thread-n這樣的访雪,沒有業(yè)務含義详瑞,不利于我們定位問題。
給線程池里的線程命名通常有兩種方式:
- 利用谷歌的ThreadFactoryBuilder給線程池里的線程命名
public static void main(String[] args) throws Exception {
ThreadFactory threadFactory =
new ThreadFactoryBuilder().setNameFormat("用來測似的線程池" + "-%d").setDaemon(true).build();
ThreadPoolExecutor threadPoolExecutor =
new ThreadPoolExecutor(2,10,1l,TimeUnit.SECONDS,new ArrayBlockingQueue<>(10),threadFactory);
for(int i = 0;i<10;i++){
threadPoolExecutor.execute(()->{
System.out.println("當前線程名:"+Thread.currentThread().getName());
});
}
}
如上代碼就是命名了臣缀,下面是如果這個線程池里的線程報錯坝橡,可以很容易定位。
- 也可以自己實現(xiàn)ThreadFactor來給線程池里的線程命名
public class Test {
public static void main(String[] args) throws Exception {
MyThreadFactory threadFactory = new MyThreadFactory(Executors.defaultThreadFactory(),"測試線程池");
ThreadPoolExecutor threadPoolExecutor =
new ThreadPoolExecutor(2,10,1l,TimeUnit.SECONDS,new ArrayBlockingQueue<>(10),threadFactory);
for(int i = 0;i<10;i++){
threadPoolExecutor.execute(()->{
System.out.println("當前線程名:"+Thread.currentThread().getName());
});
}
}
}
final class MyThreadFactory implements ThreadFactory{
ThreadFactory threadFactory;
String name;
AtomicInteger i = new AtomicInteger(1);
MyThreadFactory(ThreadFactory threadFactory,String name){
this.threadFactory = threadFactory;
this.name = name;
}
@Override
public Thread newThread(Runnable r) {
Thread t = threadFactory.newThread(r);
t.setName(name+i.getAndIncrement());
return t;
}
}
其實這種寫法就是單純的包了一層精置,每一個線程都手動的setName給設置了個名字计寇。具體要用那種寫法都可以,反正我是覺得自己設置的這個最開始寫要麻煩點脂倦,但是每次用方便番宁。谷歌的方法不需要創(chuàng)建什么工具類,但是每次創(chuàng)建都要設置赖阻。
正確的配置線程池參數(shù)
常規(guī)操作
首先這里要說一個常識:并不是線程越多越好蝶押。比如一個很小的任務,一個人做要1小時火欧,但是60個人也不會是一分鐘棋电。甚至因為人多交流成本太大。指不定還會用時更長布隔。
線程數(shù)量過多的影響也是和我們分配多少人做事一樣离陶。對于多線程的場景主要是增加了上下文切換成本。
類比我們人類通過合作做某件事衅檀,我們可以知道線程池過大過小都不好招刨,合適才是最好的。
如果我們設置的線程池數(shù)量太小哀军,同一時間有大量任務/請求需要處理沉眶,可能會導致大量的請求/任務在任務隊列中排隊等待執(zhí)行打却,甚至出現(xiàn)隊列滿了之后任務/請求無法處理的情況』丫螅或者大量任務堆積在隊列中導致OOM柳击,這樣很明顯是有問題的。CPU根本沒有得到充分利用片习。
但是我們設置線程數(shù)量過大捌肴,大量線程可能同時爭奪CPU資源,這樣會導致大量的上下文切換藕咏,從而增加線程的執(zhí)行時間状知,影響整體執(zhí)行效率。
網上有一個簡單并且通用的公式:
- CPU密集型任務(n+1):這種任務消耗的是CPU資源孽查,可以將線程數(shù)設置為cpu核心數(shù)+1.比CPU核心數(shù)多一個是為了防止線程偶發(fā)的缺頁中斷或者其他原因導致的任務暫停饥悴。一旦任務暫停,cpu就會處于空閑狀態(tài)盲再,這個時候多出的一個線程就可以充分利用CPU時間西设。
- I/O密集型任務(2n):這種任務應用起來系統(tǒng)會占用大部分時間處理I/O交互,而線程處理I/O的時間段不會占用CPU來處理答朋,這時候就可以把CPU交出給其他線程使用贷揽。所以我們可以多配置一些線程。比如2N梦碗。
如何判斷是CPU密集任務還是IO密集任務擒滑?
CPU密集型簡單理解就是利用CPU計算能力的任務。比如在內存中對大量數(shù)據(jù)進行排序叉弦。但凡涉及到網絡讀取,文件讀取這類都是IO密集型藻糖。這類任務的特點是CPU計算消耗時間相比于等待IO操作完成的時間來說很少淹冰。大部分時間都花費在等待IO操作完成上。
美團的騷操作
美團技術團隊在java線程池實現(xiàn)原理以及在內圖案業(yè)務中的實踐這篇文章中介紹到對線程池參數(shù)實現(xiàn)可自定義配置的思路和方法巨柒。
美團技術團隊的思路是主要對線程池的核心參數(shù)實現(xiàn)自定義可配置樱拴。這三個核心參數(shù)是:
- corPoolSize:核心線程數(shù)定義了最小可以同時運行的線程數(shù)量。
- maximumPoolSize:當隊列中存放的任務達到隊列容量時洋满,當前可以同事運行的線程數(shù)量變?yōu)樽畲缶€程數(shù)晶乔。
- workQueue:當新任務來的時候會先判斷當前運行的線程數(shù)量是否達到核心線程數(shù),如果達到的話新任務會被存放在隊列中牺勾。
- 還包括一些隊列長度等正罢。
實現(xiàn)的重點是基于ThreadPoolExecutor的幾個方法,我們只需要維護ThreadPoolExecutor的實例驻民,并在需要修改的時候拿到實例修改其參數(shù)即可翻具。基于這個原理我們做到線程池參數(shù)的動態(tài)化裆泳,可視并且可配置叹洲。效果如下圖工禾。
其實我們基于這個思想可以做的就更多了。比如一些監(jiān)控:線程池活躍度闻葵,告警,執(zhí)行任務的頻率和耗時笙隙,Reject異常等等。從而避免故障或者加速故障的修復竟痰。感覺美團技術團隊對于線程池的實踐介紹比較淺顯易懂,感興趣的可以自己去看下坏快。
本篇筆記就記到這里铅檩,如果稍微幫到你了記得點個喜歡點個關注。也祝大家工作順順利利莽鸿,生活健健康康~昧旨!