Java19 帶來的虛擬線程是怎樣玩出花提升十倍性能的
今天阿粉想跟大家聊的時(shí)候 Java19 中提到的虛擬線程 virtual threads。
基本概念
我們都知道 Java 中的線程跟操作系統(tǒng)的內(nèi)核線程是一對一的,Java 線程的調(diào)度其實(shí)是依賴操作系統(tǒng)的內(nèi)核線程的,這就導(dǎo)致了我們的線程切換和運(yùn)行就需要進(jìn)行上下文切換以及消耗大量的系統(tǒng)資源,同時(shí)我們也知道機(jī)器的資源是昂貴的并且也是有限的,我們不能也無法肆無忌憚的創(chuàng)建線程,因此線程往往會成為我們系統(tǒng)的瓶頸。
為了解決這個(gè)問題,Java19 中提出了一種虛擬線程的概念,為了區(qū)別,之前的線程被稱為平臺線程。要注意虛擬線程并不是用來直接取代平臺線程的,虛擬線程是建議在平臺線程之上的,一個(gè)平臺線程可以對應(yīng)多個(gè)虛擬線程,同時(shí)一個(gè)平臺線程還是一一對應(yīng)內(nèi)核線程,因此上面的架構(gòu)就變成了如下,一個(gè) VT 代表一個(gè)虛擬線程。
如果有小伙伴對 GO 語言比較熟悉的話,就會想到 Java 中的虛擬線程跟 GO 中的 Goroutines 是很類似的,確實(shí)是這樣,所以說語言都是相通的。
舉個(gè)栗子
這里我們通過分別使用平臺線程以及虛擬線程來測試一個(gè) case 看看兩者的耗時(shí)和性能是怎樣的,測試分如下幾步,我們依次來看一下。注意下面的測試代碼都是在 Java19 的版本中運(yùn)行的。
平臺線程方式
我們通過 JDK 自帶的線程池 Executors.newCachedThreadPool() 來創(chuàng)建線程池,并執(zhí)行一定數(shù)據(jù)任務(wù),任務(wù)的數(shù)量我們通過入?yún)砜刂疲奖愫罄m(xù)通過主函數(shù)調(diào)用。
虛擬線程的方式
虛擬線程的代碼跟上面的代碼十分相似,代碼如下??梢钥吹?,在代碼層面上跟上面唯一的區(qū)別就是 Executors.newCachedThreadPool() 這一行變成了 Executors.newVirtualThreadPerTaskExecutor() 即代表創(chuàng)建的虛擬線程。
監(jiān)控運(yùn)行的線程
上面的兩個(gè)方法都是都是創(chuàng)建線程池用來提交任務(wù)的,但是位于具體創(chuàng)建了多少個(gè)線程我們是不知道的,所以我們還需要通過下面的代碼來監(jiān)控。
通過另一個(gè)線程池開啟一個(gè)線程信息監(jiān)控的線程,每秒鐘輸出一次當(dāng)前的運(yùn)行線程數(shù)。這里注意,如果上面的代碼在 IDEA 中提示報(bào)錯(cuò),找不到類,如下所示,我們可以將鼠標(biāo)放上去進(jìn)行修復(fù)。
也可以手動在設(shè)置中的編譯器》Java 編譯器這里給自己的模塊增加一個(gè)編譯參數(shù) -parameters --add-modules java.management --enable-preview 。
運(yùn)行
上面的三段組合在一起就是一個(gè)完整的 case,如果這個(gè)時(shí)候如果上面的代碼都正常,在運(yùn)行的時(shí)候不出意外會出現(xiàn)下面的錯(cuò)誤,
這里是因?yàn)楫?dāng)前 Java19 中的虛擬線程特性還處于預(yù)覽階段,不能直接使用,我們需要在啟動參數(shù)上面配置 --enable-preview 參數(shù),才能正常測試,如下所示,不同版本的 IDEA 可能顯示的位置不一樣,但是都是配置 VM 參數(shù),找一下就好了。
配置好了過后再次運(yùn)行就可以得到如下的結(jié)果,可以看到在 size 大小為 100000 的情況下,虛擬線程只創(chuàng)建了 12 個(gè)平臺線程,并且只在 2523 ms 就完成了整個(gè)任務(wù)。
但是當(dāng)我們運(yùn)行平臺線程的方法的時(shí)候會發(fā)現(xiàn),同樣的 size 的情況下,平臺線程創(chuàng)建了好幾千個(gè),而且還會觸發(fā) OOM,因?yàn)椴僮飨到y(tǒng)的資源已經(jīng)被耗盡了,由此可見虛擬線程的性能要遠(yuǎn)遠(yuǎn)高于平臺線程。YYDS!
[ ] 為了避免OOM 我們也可以將代碼中的 Executors.newCachedThreadPool() 方法,改成 Executors.newFixedThreadPool(xxx),這樣雖然可以避免大量創(chuàng)建線程導(dǎo)致 OOM,但是任務(wù)執(zhí)行的時(shí)長就會消耗更長,阿粉這邊測試在 size 為 10000 的情況下,配置 500 個(gè)線程的時(shí)候,總共花費(fèi)了 20276 ms,在數(shù)據(jù)量小十倍的情況下耗時(shí)卻增長十倍。性能可想而知,感興趣的小伙伴可以自己嘗試一下。