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