JAVA 21 最重要的特性之一就是虛擬線程 (JEP 444)。這些輕量級(jí)的線程降低了編寫、維護(hù)和觀察高吞吐量并行應(yīng)用所需的努力。
在討論新特性之前,讓我們先看一下當(dāng)前的狀態(tài),以便更好地理解它試圖解決什么問題以及帶來了哪些好處。
平臺(tái)線程
在引入虛擬線程之前,我們習(xí)慣使用的線程是 java.lang.Thread,它背后是所謂的平臺(tái)線程 (platform threads)。
這些線程通常與操作系統(tǒng)調(diào)度的內(nèi)核線程一一映射。操作系統(tǒng)線程相當(dāng)“重”,這使得它們適合執(zhí)行所有類型的任務(wù)。
根據(jù)操作系統(tǒng)和配置,它們默認(rèn)情況下會(huì)消耗大約2到10 MB的內(nèi)存。因此,如果你想在高負(fù)載并發(fā)應(yīng)用程序中使用一百萬(wàn)個(gè)線程,最好要有超過2 TB的可用內(nèi)存!
這存在一個(gè)明顯的瓶頸,限制了我們實(shí)際可以在沒有缺點(diǎn)的情況下?lián)碛械木€程數(shù)量。
每個(gè)請(qǐng)求一個(gè)線程
這很成問題,因?yàn)樗苯优c典型的服務(wù)器應(yīng)用程序“每個(gè)請(qǐng)求一個(gè)線程”的方法相沖突。使用每個(gè)請(qǐng)求一個(gè)線程有很多優(yōu)點(diǎn),例如更簡(jiǎn)單的狀態(tài)管理和清理。但它也創(chuàng)造了可擴(kuò)展性限制。應(yīng)用程序的“并發(fā)單位”,在這種情況下是一個(gè)請(qǐng)求,需要一個(gè)“平臺(tái)并發(fā)單位”。因此,線程很容易被原始CPU能力或網(wǎng)絡(luò)耗盡。
即使“每個(gè)請(qǐng)求一個(gè)線程”有許多優(yōu)點(diǎn),共享重量級(jí)的線程可以更均勻地利用硬件,但也需要一種完全不同的方法。
異步救援
而不是在單個(gè)線程上運(yùn)行整個(gè)請(qǐng)求,它的每個(gè)部分都從池中使用一個(gè)線程,當(dāng)它們的任務(wù)完成時(shí),另一個(gè)任務(wù)可能會(huì)重用同一個(gè)線程。這允許代碼需要更少的線程,但引入了異步編程的負(fù)擔(dān)。
異步編程伴隨著它自己的范例,具有一定的學(xué)習(xí)曲線,并且可能會(huì)使程序更難理解和跟蹤。請(qǐng)求的每個(gè)部分可能都在不同的線程上執(zhí)行,從而創(chuàng)建沒有合理上下文的堆棧跟蹤,并使調(diào)試某些內(nèi)容變得非常棘手甚至幾乎不可能。
Java有一個(gè)用于異步編程的優(yōu)秀API,CompletableFuture。但這是一個(gè)復(fù)雜的API,并且不太適合許多Java開發(fā)人員習(xí)慣的思維方式。
重新審視“每個(gè)請(qǐng)求一個(gè)線程”模型,很明顯,一種更輕量級(jí)的線程方法可以解決瓶頸并提供一種熟悉的做事方式。
輕量級(jí)線程
由于平臺(tái)線程的數(shù)量是無(wú)法在沒有更多硬件的情況下改變的,因此需要另一個(gè)抽象層,切斷可怕的 1:1 映射,它是首先造成瓶頸的原因。
輕量級(jí)線程不與特定的平臺(tái)線程綁定,也不會(huì)伴隨大量的預(yù)分配內(nèi)存。它們由運(yùn)行時(shí)而不是底層操作系統(tǒng)調(diào)度和管理。這就是為什么可以創(chuàng)建大量輕量級(jí)線程的原因。
這個(gè)概念并不新鮮,許多語(yǔ)言都采用某種形式的輕量級(jí)線程:
- Go 語(yǔ)言中的 Goroutine
- Erlang 進(jìn)程
- Haskell 線程
- 等等
Java最終于第21版中引入了自己的輕量級(jí)線程實(shí)現(xiàn):虛擬線程 (Virtual Threads)。
虛擬線程
虛擬線程是一種新的輕量級(jí)java.lang.Thread變體,是Project Loom的一部分,它不是由操作系統(tǒng)管理或調(diào)度的。相反,JVM負(fù)責(zé)調(diào)度。
當(dāng)然,任何實(shí)際的工作都必須在平臺(tái)線程中運(yùn)行,但是JVM使用所謂的“載體線程”(carrier threads) 來“攜帶”任何虛擬線程,以便在它們需要執(zhí)行時(shí)執(zhí)行這些線程。
圖片
JVM/操作系統(tǒng)線程調(diào)度器
所需的平臺(tái)線程在一個(gè) FIFO 工作竊取 ForkJoinPool 中進(jìn)行管理,該池默認(rèn)情況下使用所有可用的處理器,但可以通過調(diào)整系統(tǒng)屬性jdk.virtualThreadScheduler.parallelism來根據(jù)需求進(jìn)行修改。
ForkJoinPool與其他功能(例如并行流)使用的通用池之間的主要區(qū)別在于,通用池以LIFO模式運(yùn)行。
廉價(jià)且豐富的線程
擁有廉價(jià)且輕量級(jí)的線程,可以使用“每個(gè)請(qǐng)求一個(gè)線程”模型,而不必?fù)?dān)心實(shí)際需要多少個(gè)線程。如果你的代碼在虛擬線程中調(diào)用阻塞 I/O 操作,則運(yùn)行時(shí)會(huì)掛起虛擬線程,直到它可以稍后恢復(fù)。
這樣,硬件就可以被優(yōu)化到幾乎最佳的水平,從而實(shí)現(xiàn)高水平的并發(fā)性,因此也實(shí)現(xiàn)高吞吐量。
因?yàn)樗鼈兎浅A畠r(jià),所以虛擬線程不會(huì)被重用或需要池化。每個(gè)任務(wù)都由其自己的虛擬線程表示。
設(shè)置邊界
調(diào)度器負(fù)責(zé)管理載體線程,因此需要一定的邊界和分離,以確保可能的“無(wú)數(shù)”虛擬線程按照預(yù)期運(yùn)行。這是通過在載體線程及其可能攜帶的任何虛擬線程之間不保持線程關(guān)聯(lián)來實(shí)現(xiàn)的:
- 虛擬線程無(wú)法訪問載體,Thread.currentThread() 返回虛擬線程本身。
- 堆棧跟蹤是分開的,任何在虛擬線程中拋出的異常只包含其自己的堆棧幀。
- 虛擬線程的線程局部變量對(duì)它的載體不可用,反之亦然。
- 從代碼的角度來看,載體及其虛擬線程共享一個(gè)平臺(tái)線程是不可見的。
讓我們看看代碼
使用Virtual Threads最大的好處是,你不需要學(xué)習(xí)新的范例或復(fù)雜的API,就像使用異步編程一樣。相反,你可以像對(duì)待非虛擬線程一樣處理它們。
創(chuàng)建平臺(tái)線程
創(chuàng)建平臺(tái)線程很簡(jiǎn)單,就像使用 Runnable 創(chuàng)建一樣:
Runnable fn = () -> {
// your code here
};
Thread thread = new Thread(fn).start();
隨著Project Loom簡(jiǎn)化了新的并發(fā)方法,它還提供了一種創(chuàng)建平臺(tái)支持線程的新方法:
Thread thread = Thread.ofPlatform().
.start(runnable);
實(shí)際上,現(xiàn)在還有一個(gè)完整的fluent API,因?yàn)閛fPlatform()會(huì)返回一個(gè)Thread.Builder.OfPlatform實(shí)例:
Thread thread = Thread.ofPlatform().
.daemon()
.name("my-custom-thread")
.unstarted(runnable);
但你肯定不是來學(xué)習(xí)創(chuàng)建“舊”線程的新方法的,我們想要一點(diǎn)新的東西。繼續(xù)看。
創(chuàng)建虛擬線程
對(duì)于虛擬線程,也有類似的fluent API:
Runnable fn = () -> {
// your code here
};
Thread thread = Thread.ofVirtual(fn)
.start();
除了構(gòu)建器方法之外,你還可以直接使用以下方式執(zhí)行Runnable:
Thread thread = Thread.startVirtualThread(() -> {
// your code here
});
由于所有虛擬線程始終是守護(hù)線程,因此如果你想在主線程上等待,請(qǐng)不要忘記調(diào)用join()。
創(chuàng)建虛擬線程的另一種方法是使用 Executor:
var executorService = Executors.newVirtualThreadPerTaskExecutor();
executorService.submit(() -> {
// your code here
});
小結(jié)
盡管Scoped Values (JEP 446) 和Structured Concurrency (JEP 453) 仍然是Java 21中的預(yù)覽功能,但Virtual Threads已經(jīng)成為一個(gè)成熟的、適用于生產(chǎn)環(huán)境的功能。
它們是Java并發(fā)的一種通用且強(qiáng)大的新方法,將對(duì)我們未來的程序產(chǎn)生重大影響。它們使用了熟悉的和可靠的“每個(gè)請(qǐng)求一個(gè)線程”方法,同時(shí)以最優(yōu)化的方式利用所有可用硬件,而不需要學(xué)習(xí)新的范例或復(fù)雜的API。