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