來源:juejin.cn/post/6948034657321484318
可能很多人都看到過一個執行緒數設定的理論:
- CPU 密集型的程式 - 核心數 + 1
- I/O 密集型的程式 - 核心數 * 2
不會吧,不會吧,真的有人按照這個理論規劃執行緒數?
執行緒數和CPU利用率的小測驗
拋開一些作業系統,計算機原理不談,說一個基本的理論(不用糾結是否嚴謹,只為好理解):一個CPU核心,單位時間內只能執行一個執行緒的指令那么理論上,我一個執行緒只需要不停的執行指令,就可以跑滿一個核心的利用率,
來寫個死回圈空跑的例子驗證一下:
測驗環境:AMD Ryzen 5 3600, 6 - Core, 12 - Threads
public class CPUUtilizationTest {
public static void main(String[] args) {
//死回圈,什么都不做
while (true){
}
}
}
運行這個例子后,來看看現在CPU的利用率:

從圖上可以看到,我的3號核心利用率已經被跑滿了
那基于上面的理論,我多開幾個執行緒試試呢?
public class CPUUtilizationTest {
public static void main(String[] args) {
for (int j = 0; j < 6; j++) {
new Thread(new Runnable() {
@Override
public void run() {
while (true){
}
}
}).start();
}
}
}
此時再看CPU利用率,1/2/5/7/9/11 幾個核心的利用率已經被跑滿:

那如果開12個執行緒呢,是不是會把所有核心的利用率都跑滿?答案一定是會的:

如果此時我把上面例子的執行緒數繼續增加到24個執行緒,會出現什么結果呢?

從上圖可以看到,CPU利用率和上一步一樣,還是所有核心100%,不過此時負載已經從11.x增加到了22.x(load average解釋參考scoutapm.com/blog/unders…),說明此時CPU更繁忙,執行緒的任務無法及時執行,
現代CPU基本都是多核心的,比如我這里測驗用的AMD 3600,6核心12執行緒(超執行緒),我們可以簡單的認為它就是12核心CPU,那么我這個CPU就可以同時做12件事,互不打擾,
如果要執行的執行緒大于核心數,那么就需要通過作業系統的調度了,作業系統給每個執行緒分配CPU時間片資源,然后不停的切換,從而實作“并行”執行的效果,
但是這樣真的更快嗎?從上面的例子可以看出,一個執行緒 就可以把一個核心 的利用率跑滿,如果每個執行緒都很“霸道”,不停的執行指令,不給CPU空閑的時間,并且同時執行的執行緒數大于CPU的核心數,就會導致作業系統更頻繁的執行切換執行緒執行 ,以確保每個執行緒都可以得到執行,
不過切換是有代價的,每次切換會伴隨著暫存器資料更新,記憶體頁表更新等操作 ,雖然一次切換的代價和I/O操作比起來微不足道,但如果執行緒過多,執行緒切換的過于頻繁,甚至在單位時間內切換的耗時已經大于程式執行的時間,就會導致CPU資源過多的浪費在背景關系切換上,而不是在執行程式,得不償失,
上面死回圈空跑的例子,有點過于極端了,正常情況下不太可能有這種程式,
大多程式在運行時都會有一些 I/O操作,可能是讀寫檔案,網路收發報文等,這些 I/O 操作在進行時時需要等待反饋的,比如網路讀寫時,需要等待報文發送或者接收到,在這個等待程序中,執行緒是等待狀態,CPU沒有作業,此時作業系統就會調度CPU去執行其他執行緒的指令,這樣就完美利用了CPU這段空閑期,提高了CPU的利用率,
上面的例子中,程式不停的回圈什么都不做,CPU要不停的執行指令,幾乎沒有啥空閑的時間,如果插入一段I/O操作呢,I/O 操作期間 CPU是空閑狀態,CPU的利用率會怎么樣呢?先看看單執行緒下的結果:
public class CPUUtilizationTest {
public static void main(String[] args) throws InterruptedException {
for (int n = 0; n < 1; n++) {
new Thread(new Runnable() {
@Override
public void run() {
while (true){
//每次慷訓圈 1億 次后,sleep 50ms,模擬 I/O等待、切換
for (int i = 0; i < 100_000_000l; i++) {
}
try {
Thread.sleep(50);
}
catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}
}
}

哇,唯一有利用率的9號核心,利用率也才50%,和前面沒有sleep的100%相比,已經低了一半了,現在把執行緒數調整到12個看看:

單個核心的利用率60左右,和剛才的單執行緒結果差距不大,還沒有把CPU利用率跑滿,現在將執行緒數增加到18:

此時單核心利用率,已經接近100%了,由此可見,當執行緒中有 I/O 等操作不占用CPU資源時,作業系統可以調度CPU可以同時執行更多的執行緒,
現在將I/O事件的頻率調高看看呢,把回圈次數減到一半,50_000_000,同樣是18個執行緒:

此時每個核心的利用率,大概只有70%左右了,
執行緒數和CPU利用率的小總結
上面的例子,只是輔助,為了更好的理解執行緒數/程式行為/CPU狀態的關系,來簡單總結一下:
- 一個極端的執行緒(不停執行“計算”型操作時),就可以把單個核心的利用率跑滿,多核心CPU最多只能同時執行等于核心數的“極端”執行緒數
- 如果每個執行緒都這么“極端”,且同時執行的執行緒數超過核心數,會導致不必要的切換,造成負載過高,只會讓執行更慢
- I/O 等暫停類操作時,CPU處于空閑狀態,作業系統調度CPU執行其他執行緒,可以提高CPU利用率,同時執行更多的執行緒
- I/O 事件的頻率頻率越高,或者等待/暫停時間越長,CPU的空閑時間也就更長,利用率越低,作業系統可以調度CPU執行更多的執行緒
執行緒數規劃的公式
前面的鋪墊,都是為了幫助理解,現在來看看書本上的定義,《Java 并發編程實戰》介紹了一個執行緒數計算的公式:
Ncpu=CPU核心數Ncpu=CPU 核心數Ncpu=CPU核心數
Ucpu=目標CPU利用率,0<=Ucpu<=1Ucpu=目標CPU利用率,0<=Ucpu<=1Ucpu=目標CPU利用率,0<=Ucpu<=1
WC=等待時間和計算時間的比例\frac{W}{C}=等待時間和計算時間的比例CW=等待時間和計算時間的比例
如果希望程式跑到CPU的目標利用率,需要的執行緒數公式為:
Nthreads=Ncpu?Ucpu?(1+WC)Nthreads=NcpuUcpu(1+\frac{W}{C})Nthreads=Ncpu?Ucpu?(1+CW)
公式很清晰,現在來帶入上面的例子試試看:
如果我期望目標利用率為90%(多核90),那么需要的執行緒數為:
核心數12 * 利用率0.9 * (1 + 50(sleep時間)/50(回圈50_000_000耗時)) ≈ 22
現在把執行緒數調到22,看看結果:

現在CPU利用率大概80+,和預期比較接近了,由于執行緒數過多,還有些背景關系切換的開銷,再加上測驗用例不夠嚴謹,所以實際利用率低一些也正常,
把公式變個形,還可以通過執行緒數來計算CPU利用率:
Ucpu=NthreadsNcpu?(1+WC)Ucpu=\frac{Nthreads}{Ncpu*(1+\frac{W}{C})}Ucpu=Ncpu?(1+CW)Nthreads
執行緒數22 / (核心數12 * (1 + 50(sleep時間)/50(回圈50_000_000耗時))) ≈ 0.9
雖然公式很好,但在真實的程式中,一般很難獲得準確的等待時間和計算時間,因為程式很復雜,不只是“計算” ,一段代碼中會有很多的記憶體讀寫,計算,I/O 等復合操作,精確的獲取這兩個指標很難,所以光靠公式計算執行緒數過于理想化,
真實程式中的執行緒數
那么在實際的程式中,或者說一些Java的業務系統中,執行緒數(執行緒池大小)規劃多少合適呢?
先說結論:沒有固定答案,先設定預期,比如我期望的CPU利用率在多少,負載在多少,GC頻率多少之類的指標后,再通過測驗不斷的調整到一個合理的執行緒數
比如一個普通的,SpringBoot 為基礎的業務系統,默認Tomcat容器+HikariCP連接池+G1回收器,如果此時專案中也需要一個業務場景的多執行緒(或者執行緒池)來異步/并行執行業務流程,
此時我按照上面的公式來規劃執行緒數的話,誤差一定會很大,因為此時這臺主機上,已經有很多運行中的執行緒了,Tomcat有自己的執行緒池,HikariCP也有自己的后臺執行緒,JVM也有一些編譯的執行緒,連G1都有自己的后臺執行緒,這些執行緒也是運行在當前行程、當前主機上的,也會占用CPU的資源,
所以受環境干擾下,單靠公式很難準確的規劃執行緒數,一定要通過測驗來驗證,
流程一般是這樣:
-
分析當前主機上,有沒有其他行程干擾
-
分析當前JVM行程上,有沒有其他運行中或可能運行的執行緒
-
設定目標
-
- 目標CPU利用率 - 我最高能容忍我的CPU飆到多少?
- 目標GC頻率/暫停時間 - 多執行緒執行后,GC頻率會增高,最大能容忍到什么頻率,每次暫停時間多少?
- 執行效率 - 比如批處理時,我單位時間內要開多少執行緒才能及時處理完畢
- ……
-
梳理鏈路關鍵點,是否有卡脖子的點,因為如果執行緒數過多,鏈路上某些節點資源有限可能會導致大量的執行緒在等待資源(比如三方介面限流,連接池數量有限,中間件壓力過大無法支撐等)
-
不斷的增加/減少執行緒數來測驗,按最高的要求去測驗,最侄訓得一個“滿足要求”的執行緒數**
而且而且而且!不同場景下的執行緒數理念也有所不同:
- Tomcat中的maxThreads,在Blocking I/O和No-Blocking I/O下就不一樣
- Dubbo 默認還是單連接呢,也有I/O執行緒(池)和業務執行緒(池)的區分,I/O執行緒一般不是瓶頸,所以不必太多,但業務執行緒很容易稱為瓶頸
- Redis 6.0以后也是多執行緒了,不過它只是I/O 多執行緒,“業務”處理還是單執行緒
所以,不要糾結設定多少執行緒了,沒有標準答案,一定要結合場景,帶著目標,通過測驗去找到一個最合適的執行緒數,
可能還有同學可能會有疑問:“我們系統也沒啥壓力,不需要那么合適的執行緒數,只是一個簡單的異步場景,不影響系統其他功能就可以”
很正常,很多的內部業務系統,并不需要啥性能,穩定好用符合需求就可以了,那么我的推薦的執行緒數是:CPU核心數
附錄
Java 獲取CPU核心數
Runtime.getRuntime().availableProcessors()//獲取邏輯核心數,如6核心12執行緒,那么回傳的是12
Linux 獲取CPU核心數
# 總核數 = 物理CPU個數 X 每顆物理CPU的核數
# 總邏輯CPU數 = 物理CPU個數 X 每顆物理CPU的核數 X 超執行緒數
# 查看物理CPU個數
cat /proc/cpuinfo| grep "physical id"| sort| uniq| wc -l
# 查看每個物理CPU中core的個數(即核數)
cat /proc/cpuinfo| grep "cpu cores"| uniq
# 查看邏輯CPU的個數
cat /proc/cpuinfo| grep "processor"| wc -l
感謝您的閱讀,也歡迎您發表關于這篇文章的任何建議,關注我,技術不迷茫!小編到你上高速,
近期熱文推薦:
1.1,000+ 道 Java面試題及答案整理(2021最新版)
2.別在再滿屏的 if/ else 了,試試策略模式,真香!!
3.臥槽!Java 中的 xx ≠ null 是什么新語法?
4.Spring Boot 2.5 重磅發布,黑暗模式太炸了!
5.《Java開發手冊(嵩山版)》最新發布,速速下載!
覺得不錯,別忘了隨手點贊+轉發哦!
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/325233.html
標籤:Java
上一篇:1萬字長文詳解Redis6中記憶體淘汰演算法/持久化機制/多執行緒模型
下一篇:JVM記憶體區域
