執行緒到底設定數量多少合適
網上有很多文章說設定執行緒數的理論(個人不信):
CPU密集型的程式 - 核心數 + 1
I/O密集型的程式 - 核心數 * 2
CPU利用率
在分析執行緒數之前,先說一個基本的理論:
一個CPU核心,單位時間內只能執行一個執行緒的指令
那麼理論上,我一個執行緒只需要不停的執行指令,就可以跑滿一個核心的利用率。
來寫個死迴圈空跑的例子驗證一下:
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(newRunnable(){ @Override public void run(){ while(true){ } } }).start(); } }}
此時再看CPU利用率,1/2/5/7/9/11 幾個核心的利用率已經被跑滿:
那如果開12個執行緒呢,是不是會把所有核心的利用率都跑滿?答案一定是會的:
如果此時我把上面例子的執行緒數繼續增加到24個執行緒,會出現什麼結果呢?
CPU利用率和上一步一樣,還是所有核心100%,不過此時負載從11.x增加到了22.x,說明此時CPU更繁忙,執行緒的任務無法及時執行。
現代CPU基本都是多核心的,我們可以簡單的認為是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(newRunnable(){ @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 併發程式設計實戰》中介紹了一個執行緒數計算的公式
如果希望程式跑到CPU的目標利用率,需要的執行緒數公式為:
公式很清晰,現在來帶入上面的例子試試看:
如果我期望目標利用率為90%(多核90),那麼需要的執行緒數為:
核心數12 * 利用率0.9 * (1 + 50(sleep時間)/50(迴圈50_000_000耗時)) ≈ 22
分析一下,W是等待時間比如IO等待的時間,C是計算時間即我完成那麼多次迴圈一共耗時是多少
現在把執行緒數調到22,看看結果:
現在CPU利用率大概80+,和預期比較接近了,由於執行緒數過多,還有些上下文切換的開銷,再加上測試用例不夠嚴謹,所以實際利用率低一些也正常。
真實程式中的執行緒數是怎樣的
那麼在實際的程式中,或者說一些Java的業務系統中,執行緒數(執行緒池大小)規劃多少合適呢?
先說結論:沒有固定答案,先設定預期,比如我期望的CPU利用率在多少,負載在多少,GC頻率多少之類的指標後,再透過測試不斷的調整到一個合理的執行緒數
比如一個普通的,SpringBoot 為基礎的業務系統,預設Tomcat容器+連線池+垃圾回收器,如果此時專案中也需要一個業務場景的多執行緒(或者執行緒池)來非同步/並行執行業務流程。
此時我按照上面的公式來規劃執行緒數的話,誤差一定會很大。因為此時這臺主機上,已經有很多執行中的執行緒了,Tomcat有自己的執行緒池,JVM也有一些執行緒,垃圾回收器都有自己的後臺執行緒。這些執行緒也是執行在當前程序、當前主機上的,也會佔用CPU的資源。
所以受環境干擾下,單靠公式很難準確的規劃執行緒數,一定要透過測試來驗證。
流程一般是這樣:
分析當前主機上,有沒有其他程序干擾
分析當前JVM程序上,有沒有其他執行中或可能執行的執行緒
設定目標
目標CPU利用率 - 我最高能容忍我的CPU飆到多少?
目標GC頻率/暫停時間 - 多執行緒執行後,GC頻率會增高,最大能容忍到什麼頻率,每次暫停時間多少?
執行效率 - 比如批處理時,我單位時間內要開多少執行緒才能及時處理完畢
……
所以,不要糾結設定多少執行緒了。沒有標準答案,一定要結合場景,帶著目標,透過測試去找到一個最合適的執行緒數。
可能會有疑問:“我們系統也沒啥壓力,不需要那麼合適的執行緒數,只是一個簡單的非同步場景,不影響系統其他功能就可以”
很正常,不需要啥效能,穩定好用符合需求就可以了。那麼我的推薦的執行緒數是:CPU核心數