可能不少人都看到過一個線程數設置的理論:java
不會吧,不會吧,真的有人按照這個理論規劃線程數?編程
拋開一些操做系統,計算機原理不談,說一個基本的理論(不用糾結是否嚴謹,只爲好理解):bash
一個CPU核心,單位時間內只能執行一個線程的指令網絡
那麼理論上,我一個線程只須要不停的執行指令,就能夠跑滿一個核心的利用率。多線程
來寫個死循環空跑的例子驗證一下:併發
測試環境:AMD Ryzen 5 3600, 6 - Core, 12 - Threads異步
public class CPUUtilizationTest { public static void main(String[] args) { //死循環,什麼都不作 while (true){ } } }
運行這個例子後,來看看如今CPU的利用率:ide
從圖上能夠看到,個人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解釋參考https://scoutapm.com/blog/understanding-load-averages),說明此時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狀態的關係,來簡單總結一下:
I/O 事件的頻率頻率越高,或者等待/暫停時間越長,CPU的空閒時間也就更長,利用率越低,操做系統能夠調度CPU執行更多的線程
前面的鋪墊,都是爲了幫助理解,如今來看看書本上的定義。《Java 併發編程實戰》介紹了一個線程數計算的公式:
$$Ncpu=CPU 核心數$$
$$Ucpu=目標CPU利用率,0<=Ucpu<=1$$
$$\frac{W}{C}=等待時間和計算時間的比例$$
若是但願程序跑到CPU的目標利用率,須要的線程數公式爲:
$$Nthreads=Ncpu*Ucpu*(1+\frac{W}{C})$$
公式很清晰,如今來帶入上面的例子試試看:
若是我指望目標利用率爲90%(多核90),那麼須要的線程數爲:
核心數12 利用率0.9 (1 + 50(sleep時間)/50(循環50_000_000耗時)) ≈ 22
如今把線程數調到22,看看結果:
如今CPU利用率大概80+,和預期比較接近了,因爲線程數過多,還有些上下文切換的開銷,再加上測試用例不夠嚴謹,因此實際利用率低一些也正常。
把公式變個形,還能夠經過線程數來計算CPU利用率:
$$Ucpu=\frac{Nthreads}{Ncpu*(1+\frac{W}{C})}$$
線程數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的資源。
不過對於業務系統來講,通常這些線程不太會成爲瓶頸
因此受環境干擾下,單靠公式很難準確的規劃線程數,必定要經過測試來驗證。
流程通常是這樣:
設定目標
並且並且並且!不一樣場景下的線程數理念也有所不一樣:
因此,不要糾結設置多少線程了。沒有標準答案,必定要結合場景,帶着目標,經過測試去找到一個最合適的線程數。
可能還有同窗可能會有疑問:「咱們系統也沒啥壓力,不須要那麼合適的線程數,只是一個簡單的異步場景,不影響系統其餘功能就能夠」
很正常,不少的內部業務系統,並不須要啥性能,穩定好用符合需求就能夠了。那麼個人推薦的線程數是:CPU核心數
Runtime.getRuntime().availableProcessors()//獲取邏輯核心數,如6核心12線程,那麼返回的是12
# 總核數 = 物理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
原創不易,轉載請聯繫做者。若是個人文章對您有幫助,請點贊/收藏/關注鼓勵支持一下吧❤❤❤❤❤❤