線程池大小設(shè)置多少合理
大部分讀者可能都會(huì)看過(guò)網(wǎng)上的幾篇文章,對(duì)于線程數(shù)的設(shè)定基本都是采用下面這個(gè)公式:
計(jì)算密集型=CPU核心數(shù)+1
IO密集型=CPU核心數(shù)*2+1然而事實(shí)真的是這樣嗎?那么為什么tomcat服務(wù)器的核心線程數(shù)要設(shè)置為200呢?基于此問(wèn)題,筆者也基于個(gè)人的經(jīng)驗(yàn)和實(shí)踐給出自己的一套方法論,希望對(duì)你有幫助。

一、線程池調(diào)測(cè)實(shí)踐
1. 單計(jì)算任務(wù)是否可以跑滿單個(gè)CPU
針對(duì)上述的公式,作者認(rèn)為計(jì)算密集型的任務(wù)基本都在進(jìn)行CPU運(yùn)算,沒(méi)有所謂的IO等待,所以設(shè)置線程池參數(shù)時(shí),只需設(shè)置為:
CPU核心數(shù)+1注意,這里的加1是為了保證及時(shí)因?yàn)榕及l(fā)的缺頁(yè)中斷亦或者某些異常導(dǎo)致某個(gè)線程消亡,也能利用額外的一個(gè)線程跑滿CPU時(shí)鐘周期,以確保在單位時(shí)間內(nèi)盡可能的利用到CPU。
所以我們是否可以得出這個(gè)結(jié)果,如果我們的計(jì)算密集型的任務(wù)不斷的循環(huán)跑,它就能跑滿單個(gè)CPU呢?
對(duì)此我們給出下面這樣一段代碼,在web程序啟動(dòng)之后直接無(wú)限循環(huán):
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class ThreadPoolApplication {
public static void main(String[] args) {
SpringApplication.run(ThreadPoolApplication.class, args);
//空跑一個(gè)循環(huán)
while (true) {
}
}
}
}將程序部署到服務(wù)器上啟動(dòng),可以看到在筆者16核的服務(wù)器上,Cpu6 跑滿100%,很明顯我們的程序霸占了這個(gè)CPU核心,由此可以印證對(duì)于CPU在單位時(shí)間內(nèi)只能指向一個(gè)線程的指令:

2. 密集計(jì)算任務(wù)與CPU調(diào)度的關(guān)系
有了上面的理論基礎(chǔ),我們將線程數(shù)設(shè)置為CPU核心數(shù)的一半,看看當(dāng)前的服務(wù)器的運(yùn)行情況:
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class ThreadPoolApplication {
public static void main(String[] args) {
SpringApplication.run(ThreadPoolApplication.class, args);
//創(chuàng)建CPU核心數(shù)一半的線程
for (int i = 0; i < Runtime.getRuntime().availableProcessors() >> 1; i++) {
new Thread(() -> {
//空跑一個(gè)循環(huán)
while (true) {
}
}).start();
}
}
}和預(yù)測(cè)的結(jié)果一樣,對(duì)于計(jì)算密集型的任務(wù)而言,每一個(gè)空循環(huán)的線程(即每一個(gè)線程的指令都會(huì)綁定一個(gè)CPU核心):

這也就意味著,對(duì)于計(jì)算型的任務(wù),在滿載運(yùn)行的情況下,可以完全利用單個(gè)CPU核心,由此也可推出,對(duì)于計(jì)算密集型的任務(wù),滿載情況下,所有的CPU利用率都會(huì)達(dá)到100%。
對(duì)此我們不妨設(shè)置的更極端一點(diǎn),嘗試將線程數(shù)設(shè)置為CPU核心數(shù)的2倍:
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class ThreadPoolApplication {
public static void main(String[] args) {
SpringApplication.run(ThreadPoolApplication.class, args);
//創(chuàng)建CPU核心數(shù)2倍的線程
for (int i = 0; i < Runtime.getRuntime().availableProcessors() << 1; i++) {
new Thread(() -> {
//空跑一個(gè)循環(huán)
while (true) {
}
}).start();
}
}
}可以看到此時(shí)利用率全滿了,并且對(duì)應(yīng)的負(fù)載也非常顯著的提高了,在最近的一分鐘可以基本已經(jīng)達(dá)到CPU核心數(shù)了:

這里補(bǔ)充一下負(fù)載的概念,在單核情況下,負(fù)載的值為在0~1之間,這就意味著當(dāng)前cpu還未滿載,用一個(gè)比較通俗的比喻,假如單核CPU的負(fù)載值為0.5,這就意味著單條車道上有一半的車流經(jīng)過(guò),還可以容納一半的車駛?cè)耄?/p>

業(yè)內(nèi)普遍認(rèn)為在單核CPU的場(chǎng)景下,負(fù)載處于0.7是比較正常標(biāo)準(zhǔn),如果超過(guò)這個(gè)值就說(shuō)明過(guò)載了。
同理,筆者的服務(wù)器為16核,按照上述所說(shuō)我們的服務(wù)器可以看到有16條車道,所以當(dāng)負(fù)載值小于16即說(shuō)明有CPU核心未跑滿載,一旦負(fù)載超過(guò)11.2(16*0.7)就意味著我們的系統(tǒng)可能過(guò)載了。
我們將執(zhí)行計(jì)算密集型的任務(wù)的線程數(shù)設(shè)置為CPU核心數(shù)的2倍,負(fù)載不斷提升已經(jīng)超過(guò)了11.2,所以對(duì)于計(jì)算密集型任務(wù),本次線程數(shù)的設(shè)置是存在問(wèn)題的:
top - 00:21:38 up 1:09, 1 user, load average: 17.69, 10.45, 8.23自此我們就印證了為什么對(duì)于計(jì)算密集型的任務(wù),我們更簡(jiǎn)易將線程數(shù)設(shè)置為趨近于CPU核心數(shù)的原因了。
3. IO密集型任務(wù)調(diào)測(cè)
為了實(shí)現(xiàn)IO密集型實(shí)驗(yàn),筆者基于一臺(tái)8核心的服務(wù)器編寫好程序,將計(jì)算時(shí)間和IO時(shí)間盡可能的設(shè)置為五五開,如下所示,讀者可結(jié)合自身服務(wù)器性能按需調(diào)整:
public static void main(String[] args) {
SpringApplication.run(ThreadPoolApplication.class, args);
ExecutorService threadPool = Executors.newFixedThreadPool(1);
//單線程的線程池執(zhí)行一個(gè)計(jì)算和IO五五開的任務(wù)
threadPool.execute(() -> {
while (true) {
//執(zhí)行循環(huán)空跑模擬計(jì)算
for (int i = 0; i < Integer.MAX_VALUE >> 4; i++) {
for (int j = 0; j < Integer.MAX_VALUE; j++) {
}
}
//休眠50ms
ThreadUtil.sleep(50);
}
});
}性能監(jiān)控結(jié)果如下,可以看到單核CPU利用率趨近于50%:
top - 17:29:01 up 3:05, 8 users, load average: 0.66, 0.52, 0.46
Tasks: 499 total, 1 running, 498 sleeping, 0 stopped, 0 zombie
%Cpu0 : 0.0 us, 0.0 sy, 0.0 ni,100.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu1 : 0.0 us, 0.3 sy, 0.0 ni, 99.7 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu2 : 0.0 us, 0.3 sy, 0.0 ni, 99.7 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu3 : 0.0 us, 0.0 sy, 0.0 ni,100.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu4 : 0.0 us, 0.0 sy, 0.0 ni,100.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu5 : 0.0 us, 0.0 sy, 0.0 ni,100.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu6 : 0.0 us, 0.0 sy, 0.0 ni,100.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
# 趨近于50%
%Cpu7 : 43.0 us, 0.0 sy, 0.0 ni, 57.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st基于這個(gè)比例,我們將線程數(shù)設(shè)置為CPU核心數(shù)再次運(yùn)行,最終運(yùn)行結(jié)果如下,可以看到所有的CPU利用率都趨近于50%:
Tasks: 494 total, 1 running, 493 sleeping, 0 stopped, 0 zombie
%Cpu0 : 53.2 us, 0.0 sy, 0.0 ni, 46.8 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu1 : 53.7 us, 0.0 sy, 0.0 ni, 46.3 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu2 : 49.3 us, 0.0 sy, 0.0 ni, 50.7 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu3 : 52.7 us, 0.0 sy, 0.0 ni, 47.3 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu4 : 52.7 us, 0.0 sy, 0.0 ni, 47.3 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu5 : 53.3 us, 0.3 sy, 0.0 ni, 46.3 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu6 : 53.2 us, 0.0 sy, 0.0 ni, 46.8 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu7 : 53.0 us, 0.0 sy, 0.0 ni, 47.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
KiB Mem : 7995716 total, 5055564 free, 1529664 used, 1410488 buff/cache
KiB Swap: 2097148 total, 2097148 free, 0 used. 6155340 avail Mem4. 結(jié)合公式落地IO密集型任務(wù)線程池配置
根據(jù)《Java并發(fā)編程實(shí)戰(zhàn)》所說(shuō),對(duì)于IO密集型任務(wù),線程數(shù)可按照如下公式獲取
nThread=nCPU * uCPU * (1+w/c)對(duì)應(yīng)的參數(shù)含義是:
- nThread:表示程序中應(yīng)該使用的線程數(shù)量。
- nCPU:表示系統(tǒng)中可用的CPU核心數(shù)量。
- uCPU:表示每個(gè)CPU核心的利用率(通常是一個(gè)介于0到1之間的值)。
- w/c:表示程序中等待時(shí)間(wait time)與計(jì)算時(shí)間(compute time)的比率。
因?yàn)槲覀兊腃PU為8核,我們希望全部利用,假設(shè)每個(gè)利用率為90%,按照我們IO時(shí)間和計(jì)算時(shí)間五五開來(lái)算,線程數(shù)的計(jì)算公式為:
nThread=nCPU * uCPU * (1+w/c)
= 8 * 0.9 * (1+1)
= 14.4
≈ 15因此我們將線程數(shù)設(shè)置為15個(gè)再次啟動(dòng)并運(yùn)行,可以看到最終的CPU利用率和預(yù)期的基本一致,我們可能還需要結(jié)合服務(wù)器的實(shí)際使用情況進(jìn)行上下浮動(dòng)調(diào)整:
top - 19:08:52 up 3:50, 8 users, load average: 1.16, 2.44, 2.65
Tasks: 499 total, 3 running, 496 sleeping, 0 stopped, 0 zombie
%Cpu0 : 89.7 us, 0.0 sy, 0.0 ni, 10.3 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu1 : 91.4 us, 0.3 sy, 0.0 ni, 8.3 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu2 : 87.0 us, 0.0 sy, 0.0 ni, 13.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu3 : 91.7 us, 0.0 sy, 0.0 ni, 8.3 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu4 : 87.7 us, 0.0 sy, 0.0 ni, 12.3 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu5 : 91.0 us, 0.0 sy, 0.0 ni, 9.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu6 : 93.7 us, 0.0 sy, 0.0 ni, 6.3 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu7 : 86.7 us, 0.0 sy, 0.0 ni, 13.3 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
KiB Mem : 7995716 total, 5025368 free, 1558680 used, 1411668 buff/cache
KiB Swap: 2097148 total, 2097148 free, 0 used. 6125500 avail Mem二、關(guān)于線程池公式的更進(jìn)一步討論
1. 計(jì)算密集型任務(wù)的公式的推導(dǎo)
關(guān)于計(jì)算密集型任務(wù)推導(dǎo)公式,很多讀者都是死記硬背,這里讀者從這里從工作機(jī)制和公式推導(dǎo)兩個(gè)角度進(jìn)行補(bǔ)充說(shuō)明。 計(jì)算密集型任務(wù)即任務(wù)不涉及任何IO操作導(dǎo)致阻塞而讓出CPU時(shí)間片,這意味著所有的任務(wù)都必須通過(guò)CPU完成運(yùn)算才算結(jié)束,舉個(gè)例子:假設(shè)我們的服務(wù)器只有一個(gè)CPU,單個(gè)任務(wù)運(yùn)算耗時(shí)為200ms,如果有1000個(gè)任務(wù)需要執(zhí)行,無(wú)論如何這份任務(wù)在CPU上的執(zhí)行總時(shí)間都是200000ms也就是200s:

操作系統(tǒng)在這其中唯一能做的,也就是為了避免任務(wù)饑餓在某個(gè)任務(wù)執(zhí)行100ms時(shí)將其切換,執(zhí)行另外一個(gè)任務(wù),但是兩個(gè)任務(wù)總的耗時(shí)永遠(yuǎn)是400ms,且必須在CPU上執(zhí)行完成,所以即使設(shè)置再多的線程也沒(méi)有任何意義:

從公式的角度來(lái)說(shuō),對(duì)應(yīng)計(jì)算密集型的任務(wù)w也就是程序因?yàn)镮O的等待時(shí)長(zhǎng)為0,按照極限思維來(lái)算,對(duì)應(yīng)的推導(dǎo)過(guò)程如下
nThread= nCPU * uCPU * (1+w/c)
= nCPU * uCPU * (1+0/c)
= nCPU * uCPU最終推導(dǎo)出的線程數(shù)也就是基本等同于CPU核心數(shù)*CPU利用率也就是CPU核心數(shù),考慮到一些計(jì)算異常亦或者缺頁(yè)中斷等原因?qū)е戮€程消亡,我們一般會(huì)按照經(jīng)驗(yàn)法則多1個(gè)線程,這就是為什么計(jì)算密集型的公式為CPU核心數(shù)+1,同時(shí)也因?yàn)橛?jì)算密集型的任務(wù)一般不會(huì)有太大的耗時(shí),所以大部分情況下對(duì)于此類任務(wù)都沒(méi)有基于CPU利用率去限制線程數(shù)。
2. 為什么會(huì)出現(xiàn)IO密集型線程數(shù)為CPU核心數(shù)*2+1的錯(cuò)誤說(shuō)法
這個(gè)公式是很多八股文中經(jīng)常會(huì)提及到的一點(diǎn),按照筆者的推測(cè),估計(jì)是某些博主沒(méi)有真正的理解線程數(shù)推導(dǎo)公式的含義就盲目按照理想情況下所得出的,本質(zhì)上nThread= nCPU * uCPU * (1+w/c)這個(gè)公式的含義是:
利用任務(wù)IO阻塞的等待時(shí)長(zhǎng)和計(jì)算時(shí)長(zhǎng)的占比,推導(dǎo)出某些任務(wù)因?yàn)槿蝿?wù)阻塞而掛起時(shí)可以順便執(zhí)行多少計(jì)算任務(wù)
假設(shè)我們的任務(wù)查詢數(shù)據(jù)庫(kù)也就是IO耗時(shí)為100ms,計(jì)算時(shí)長(zhǎng)也是100ms,按照公式計(jì)算為:
nThread= nCPU * uCPU * (1+w/c)
= nCPU * uCPU * (1+100ms/100ms)
= nCPU * uCPU * 2可以看到最終的結(jié)果就是nCPU * uCPU * 2,是不是覺(jué)得很熟悉?沒(méi)有錯(cuò),大部分錯(cuò)誤的八股文把IO密集型任務(wù)都按照計(jì)算耗時(shí)與IO阻塞耗時(shí)五五開進(jìn)行推導(dǎo)得出nCPU * uCPU * 2,然后也學(xué)著計(jì)算密集型的套路:
- 去掉CPU利用率
- 避免缺頁(yè)中斷等異常線程數(shù)+1
最終得出2* CPU +1,所以這也正是為什么筆者一直強(qiáng)調(diào)對(duì)于一些業(yè)界正確的實(shí)踐要參考一些權(quán)威性的書籍資料去了解掌握。
反駁了CPU核心數(shù)*2+1這個(gè)公式之后,我們?cè)賮?lái)說(shuō)說(shuō)正確公式的由來(lái),在上文中筆者提到該公式本質(zhì)上就是利用任務(wù)IO等待耗時(shí)和計(jì)算耗時(shí)的占比,來(lái)推算IO阻塞期間可以提前執(zhí)行掉多少的運(yùn)算任務(wù),假設(shè)我們現(xiàn)在有這樣一個(gè)場(chǎng)景:
- 服務(wù)器CPU核心數(shù)為18
- 計(jì)算耗時(shí)為1ms
- IO耗時(shí)為200ms
實(shí)際上(1+w/c)這個(gè)過(guò)程本質(zhì)上就是在計(jì)算針對(duì)這個(gè)任務(wù),在IO阻塞期間可以提前完成多少計(jì)算操作,按照我們的計(jì)算比來(lái)說(shuō)每個(gè)任務(wù)都有200ms的IO阻塞,這也就意味著在200ms的阻塞期間,理想情況下(如果CPU全心全意只執(zhí)行我們這個(gè)程序的任務(wù)),阻塞期間可以處理w/c也就是200個(gè)計(jì)算操作:

基于上述單核的推導(dǎo)過(guò)程,我們?cè)傺a(bǔ)充CPU核心數(shù)和利用率才有了下面的公式和計(jì)算過(guò)程:
nThread= nCPU * uCPU * (1+w/c)
= 18 * 1 * (1+200ms/1ms)
≈3600按照當(dāng)前任務(wù)的說(shuō)明,我們推算:
- 理想情況下單核CPU單位時(shí)間內(nèi)可以處理1000個(gè)計(jì)算操作,換算成我們的18核服務(wù)器,也就是每秒可以處理大約18*1000也就是18000個(gè)任務(wù)。
- 基于我們推測(cè)的3600個(gè)線程數(shù),按照每個(gè)任務(wù)200ms的IO來(lái)算,每個(gè)線程1s內(nèi)可以處理大約1000/200也就是5個(gè)任務(wù),那么3600個(gè)線程大約也是可以達(dá)到18000的任務(wù)
為了印證第一點(diǎn)的預(yù)期值和我評(píng)估的線程數(shù)值一致,我們寫下下面這樣一段代碼,查看當(dāng)前線程數(shù)的設(shè)定在單位時(shí)間內(nèi)是否可以處理18000個(gè)任務(wù):
//qps 計(jì)數(shù)器
private static final AtomicInteger count = new AtomicInteger(0);
//按照 1ms cpu處理耗時(shí),推算合理運(yùn)算線程池?cái)?shù)
private static final ExecutorService threadPool = Executors.newFixedThreadPool(3600);
public static void main(String[] args) {
int taskCount = 500_0000;
//計(jì)算每秒處理的任務(wù)數(shù)
new Thread(() -> {
while (true) {
Console.log("qps:{} ", count.get());
count.getAndSet(0);
ThreadUtil.sleep(1000);
}
}).start();
for (int i = 0; i < taskCount; i++) {
threadPool.execute(() -> {
ThreadUtil.sleep(200);
count.incrementAndGet();
});
}
}最終輸出結(jié)果如下,可以看到,在程序啟動(dòng)后JIT預(yù)熱階段完成后,基于我們?cè)O(shè)定的線程數(shù)是可以完成單位時(shí)間內(nèi)執(zhí)行18000個(gè)任務(wù):

同理,我們?cè)傺a(bǔ)充一個(gè)案例來(lái)推導(dǎo)這個(gè)公式的實(shí)效性以避免欠擬合:
- 服務(wù)器18核
- 計(jì)算耗時(shí)2ms
- IO耗時(shí) 200ms
同理按照公式推導(dǎo)大約需要1800個(gè)線程,結(jié)合驗(yàn)證:
- 單核CPU單位時(shí)間內(nèi)可以處理1000/2也就是500個(gè)任務(wù),也就是最終結(jié)果應(yīng)該是9000個(gè)任務(wù)
- 我們推算出的1800個(gè)線程,單位時(shí)間內(nèi)每個(gè)線程可以處于1000/202≈5,1800*5也可以達(dá)到9000
基于筆者的機(jī)器性能,筆者也出這樣一段耗時(shí)2ms的代碼:
public static int sum() {
long begin = System.currentTimeMillis();
long sum = 0;
for (int i = 0; i < 800_0000; i++) {
sum += i;
}
long end = System.currentTimeMillis();
if (sum != 1) {
return (int) (end - begin);
}
return 0;
}同理壓測(cè)代碼如下:
//qps 計(jì)數(shù)器
private static final AtomicInteger count = new AtomicInteger(0);
//按照 1ms cpu處理耗時(shí),推算合理運(yùn)算線程池?cái)?shù)
private static final ExecutorService threadPool = Executors.newFixedThreadPool(1800);
public static void main(String[] args) {
int taskCount = 500_0000;
//計(jì)算每秒處理的任務(wù)數(shù)
new Thread(() -> {
while (true) {
Console.log("qps:{} ", count.get());
count.getAndSet(0);
ThreadUtil.sleep(1000);
}
}).start();
for (int i = 0; i < taskCount; i++) {
threadPool.execute(() -> {
sum();
ThreadUtil.sleep(200);
count.incrementAndGet();
});
}
}最終輸出結(jié)果如下,可以看到理想情況下,基于該公式是可以得到預(yù)期的結(jié)果:

三、小結(jié)
上述的線程池設(shè)置更多是基于理想情況下的調(diào)整設(shè)置,讀者在進(jìn)行壓測(cè)調(diào)整時(shí),還需要結(jié)合機(jī)器實(shí)際使用情況進(jìn)行適當(dāng)增減,所以總的來(lái)說(shuō)線程池參數(shù)的設(shè)定需要符合以下幾個(gè)原則:
- 計(jì)算密集型任務(wù)應(yīng)該為CPU核心數(shù)上下浮動(dòng)。
- IO密集型應(yīng)該通過(guò)公式2得到一個(gè)預(yù)估的值并結(jié)合生產(chǎn)環(huán)境的情況不斷測(cè)試得到一個(gè)理想的數(shù)值。
- 大部分場(chǎng)景下我們的系統(tǒng)并沒(méi)有太大的壓力,不需要那么合適的線程數(shù),對(duì)于這種簡(jiǎn)單的異步場(chǎng)景,我們只需設(shè)置為CPU核心數(shù)即可。






























