虛擬線程簡介:Java并發性的一種新方法
譯文作者 | Matthew Tyson
譯者 | 李睿
Java19影響最深遠的更新之一是引入了虛擬線程。虛擬線程是Project Loom的一部分,可以在Java19預覽版中使用。
虛擬線程如何工作
虛擬線程在操作系統進程和應用程序級并發之間引入了一個抽象層。換句話說,虛擬線程可用于調度Java虛擬機編排的任務,因此JVM在操作系統和程序之間起到中介作用。圖1展示了虛擬線程的架構。
圖1.Java中虛擬線程的架構
在這種架構中,應用程序實例化虛擬線程,并由JVM分配處理虛擬線程的計算資源。與此相比,常規線程直接映射到操作系統(OS)進程。對于常規線程,應用程序代碼負責提供和分配操作系統資源。而使用虛擬線程,應用程序可以實例化虛擬線程,從而表達并發性的需求。但正是JVM從操作系統獲取和釋放資源。
Java中的虛擬線程類似于Go語言中的goroutine。在使用虛擬線程時,JVM只能在應用程序的虛擬線程被駐留時分配計算資源,這意味著它們處于空閑狀態并等待新的事件。這種空閑在大多數服務器中是常見的:它們將一個線程分配給一個請求,然后處于空閑狀態,并等待一個新的事件,例如來自數據存儲的響應或來自網絡的進一步輸入。
使用傳統Java線程,當服務器在處理請求時處于空閑狀態時,操作系統線程也處于空閑狀態,這嚴重限制了服務器的可擴展性。正如Nicolai Parlog所解釋的那樣,“操作系統無法提高平臺線程的效率,但JDK通過切斷其線程與操作系統線程之間的一對一關系,可以更好地利用它們。”
以前為緩解與傳統Java線程相關的性能和可擴展性問題所做的努力包括異步、響應式庫(如JavaRX)。虛擬線程的不同之處在于它們是在JVM級別實現的,但是它們適合Java中現有的編程結構。
使用Java虛擬線程:演示
在這個演示中,創建了一個使用Maven原型的簡單Java應用程序。為此還做了一些更改,以便在Java19預覽版中啟用虛擬線程。一旦虛擬線程被升級到預覽之外,就不需要做這些更改了。
清單1顯示了對Maven原型的POM文件所做的更改。需要注意的是,還將編譯器設置為使用Java19,并在.mvn/jvm.config中添加了一行(例如清單2所示)。
清單1.演示應用程序的pom.xml
要使exec:java在啟用預覽的情況下工作,必須使用enable-preview開關。它使用所需的開關啟動Maven進程。
清單2.將enable preview添加到.mvn/jvm.config
現在,可以使用mvn compile exec:java執行該程序,虛擬線程特性將被編譯和執行。
使用虛擬線程的兩種方法
現在考慮在代碼中實際使用虛擬線程的兩種主要方式。雖然虛擬線程對JVM的工作方式產生了巨大的變化,但其代碼實際上與傳統Java線程非常相似。設計上的相似性使得重構現有的應用程序和服務器相對容易。這種兼容性還意味著用于監視和觀察JVM中的線程的現有工具將與虛擬線程一起工作。
Thread.startVirtualThread(Runnable r)
使用虛擬線程的最基本方法是使用Thread.startVirtualThread(Runnable r))。這是實例化線程和調用thread.start()的替代方法。查看清單3中的示例代碼。
清單3.實例化一個新線程
當帶有參數運行時,清單3中的代碼將使用一個虛擬線程,否則將使用常規線程。無論選擇哪種線程類型,該程序都會生成5萬次迭代。然后,它用隨機數做一些簡單的數學運算,并跟蹤執行所需的時間。
要使用虛擬線程運行代碼,需要鍵入:mvn-compile-exec:java-Dexec.args=“true”。要使用標準線程運行,需要鍵入:mvn-compile-exec:java。為此進行了一個快速的性能測試,得到如下結果:
- 帶有虛擬線程:Runtime: 174
- 使用常規線程:Runtime: 5450
這些結果是不科學的,但是運行時的差異是巨大的。
還有其他使用Thread生成虛擬線程的方法,例如Thread.ofVirtual().start(runnable)。
使用執行器
啟動虛擬線程的另一種主要方法是使用執行器。執行器在處理線程時很常見,它提供了一種協調許多任務和線程池的標準方法。
虛擬線程不需要使用線程池,因為創建和處理它們的成本很低,因此沒有必要使用線程池。與其相反,可以將JVM看作是管理線程池。但是,許多程序確實使用執行器,因此Java19在執行器中包含了一個新的預覽方法,使重構虛擬線程變得容易。清單4展示了新方法和舊方法。
清單4.新的執行器方法
左右滑動查看完整代碼
此外,Java19引入了Executors.newThreadPerTaskExecutor(ThreadFactory threadFactory)方法,它可以采用構建虛擬線程的ThreadFactory。這樣的線程工廠可以通過Thread.ofVirtual().factory().獲得。
虛擬線程的優秀實踐
一般來說,因為虛擬線程實現了線程類,所以它們可以在標準線程所在的任何地方使用。但是,在如何使用虛擬線程以獲得最佳效果方面存在差異。一個例子是在訪問數據存儲等資源時使用信號量來控制線程數量,而不是使用有限制的線程池。
另一個重要注意事項是,虛擬線程始終守護線程,這意味著它們將使包含它們的JVM進程保持活動狀態,直到它們完成。此外,不能更改它們的優先級。更改優先級和守護進程狀態的方法為無操作(no-ops)。
使用虛擬線程重構
虛擬線程在本質上是一個很大的改變,但它們很容易應用到現有的代碼庫中。虛擬線程將對Tomcat和GlassFish等服務器產生最大、最直接的影響。這樣的服務器應該能夠以最小的努力采用虛擬線程。在這些服務器上運行的應用程序將獲得可擴展性的收益,而無需對代碼進行任何更改,這可能對大規模應用程序產生巨大影響。考慮一個運行在多個服務器和核心上的Java應用程序,突然之間它將能夠處理一個數量級的并發請求,當然這完全取決于請求處理配置文件。
像Tomcat這樣的服務器允許帶配置參數的虛擬線程可能只是時間問題。與此同時,如果對將服務器遷移到虛擬線程感到好奇,可以閱讀Cay Horstmann撰寫的一篇博客文章,他在文章中展示了為虛擬線程配置Tomcat的過程。他啟用了虛擬線程預覽功能,并將Executor替換為只差一行的自定義實現。可擴展性的好處是顯著的,正如他在文章中所說:“通過這種更改,200個請求只需3秒,而Tomcat可以輕松處理10,000個請求。”
結論
虛擬線程是JVM的一個主要變化。對于應用程序程序員來說,它們代表了異步風格編碼(如使用回調)的另一種選擇。總之,在處理Java并發性時,可以將虛擬線程看作是一個擺向Java中同步編程范式的鐘擺。這在編程風格上大致類似于JavaScript引入的async/await(盡管在實現上完全不同)。簡而言之,使用簡單的同步語法編寫正確的異步行為變得相當容易,至少在線程花費大量時間空閑的應用程序中是這樣。
原文鏈接: