很多語(yǔ)言都有類似于“虛擬線程”的技術(shù),比如Go、C#、Erlang、Lua等,他們稱之為“協(xié)程”。 不管是虛擬線程還是協(xié)程,他們都是輕量級(jí)線程,其目的都是為了提高并發(fā)能力。 本節(jié)詳細(xì)介紹Java平臺(tái)的“虛擬線程”的技術(shù)——“JEP 425: Virtual Threads (Preview)”。
Java平臺(tái)計(jì)劃引入虛擬線程,可顯著減少編寫、維護(hù)和觀察高吞吐量并發(fā)應(yīng)用程序的工作量?!癑EP 425: Virtual Threads (Preview)”目是一個(gè)預(yù)覽性的API。
目標(biāo)
- 使以簡(jiǎn)單的線程每請(qǐng)求風(fēng)格編寫的服務(wù)器應(yīng)用程序能夠以近乎最佳的硬件利用率進(jìn)行擴(kuò)展。
- 啟用使用java.lang.Thread API的現(xiàn)有代碼,以最小的更改采用虛擬線程。
- 使用現(xiàn)有的JDK工具,輕松地對(duì)虛擬線程進(jìn)行故障排除、調(diào)試和分析。
非目標(biāo)
- 目標(biāo)不是刪除線程的傳統(tǒng)實(shí)現(xiàn),也不是靜默遷移現(xiàn)有應(yīng)用程序以使用虛擬線程。
- 改變Java的基本并發(fā)模型并不是目標(biāo)。
- 在Java語(yǔ)言或Java庫(kù)中提供新的數(shù)據(jù)并行結(jié)構(gòu)并不是目標(biāo)。Stream API仍然是并行處理大型數(shù)據(jù)集的首選方式。
動(dòng)機(jī)
近30年來(lái),Java開(kāi)發(fā)人員一直依賴線程作為并發(fā)服務(wù)器應(yīng)用程序的構(gòu)建塊。每個(gè)方法中的每個(gè)語(yǔ)句都在線程內(nèi)執(zhí)行,由于Java是多線程的,多個(gè)執(zhí)行線程同時(shí)發(fā)生。線程是Java的并發(fā)單元:一段順序代碼,與其他此類單元同時(shí)運(yùn)行,而且在很大程度上獨(dú)立于其他此類單元。每個(gè)線程都提供一個(gè)堆棧來(lái)存儲(chǔ)局部變量和協(xié)調(diào)方法調(diào)用,以及出錯(cuò)時(shí)的上下文:異常被同一線程中的方法拋出和捕獲,因此開(kāi)發(fā)人員可以使用線程的堆棧跟蹤來(lái)查找發(fā)生了什么。線程也是工具的核心概念:調(diào)試器逐步瀏覽線程方法中的語(yǔ)句,分析器可視化多個(gè)線程的行為,以幫助了解它們的性能。
線程每請(qǐng)求樣式
服務(wù)器應(yīng)用程序通常處理相互獨(dú)立的并發(fā)用戶請(qǐng)求,因此應(yīng)用程序通過(guò)在請(qǐng)求的整個(gè)持續(xù)時(shí)間內(nèi)將線程專用于該請(qǐng)求來(lái)處理請(qǐng)求是有意義的。這種線程每請(qǐng)求風(fēng)格易于理解、易于編程、易于調(diào)試和分析,因?yàn)樗褂闷脚_(tái)的并發(fā)單位來(lái)表示應(yīng)用程序的并發(fā)單位。
服務(wù)器應(yīng)用程序的可擴(kuò)展性遵循利特爾定律(Little’s Law),它與延遲、并發(fā)和吞吐量有關(guān):對(duì)于給定的請(qǐng)求處理持續(xù)時(shí)間(即延遲),應(yīng)用程序同時(shí)處理的請(qǐng)求數(shù)(即,并發(fā))必須與到達(dá)速率(即吞吐量)成比例增長(zhǎng)。例如,假設(shè)平均延遲為50ms的應(yīng)用程序通過(guò)同時(shí)處理10個(gè)請(qǐng)求,實(shí)現(xiàn)每秒200個(gè)請(qǐng)求的吞吐量。為了使該應(yīng)用程序擴(kuò)展到每秒2000個(gè)請(qǐng)求的吞吐量,它需要同時(shí)處理100個(gè)請(qǐng)求。如果每個(gè)請(qǐng)求在請(qǐng)求的持續(xù)時(shí)間內(nèi)都在線程中處理,那么,要使應(yīng)用程序跟上,線程數(shù)量必須隨著吞吐量的增長(zhǎng)而增長(zhǎng)。
不幸的是,可用線程的數(shù)量是有限的,因?yàn)镴DK將線程作為操作系統(tǒng)(OS)線程的包裝器實(shí)現(xiàn)。操作系統(tǒng)線程成本高昂,因此我們不能擁有太多線程,這使得實(shí)現(xiàn)不適合線程每請(qǐng)求風(fēng)格。如果每個(gè)請(qǐng)求在其持續(xù)時(shí)間內(nèi)消耗一個(gè)線程,從而消耗一個(gè)操作系統(tǒng)線程,那么線程數(shù)量通常在其他資源(如CPU或網(wǎng)絡(luò)連接)耗盡之前很久就成為限制因素。JDK當(dāng)前的線程實(shí)現(xiàn)將應(yīng)用程序的吞吐量限制在遠(yuǎn)低于硬件所能支持的水平。即使線程池化,也會(huì)發(fā)生這種情況,因?yàn)槌鼗兄诒苊鈫?dòng)新線程的高成本,但不會(huì)增加線程總數(shù)。
使用異步風(fēng)格提高可擴(kuò)展性
一些希望充分利用硬件的開(kāi)發(fā)人員放棄了每請(qǐng)求線程風(fēng)格,轉(zhuǎn)到線程共享風(fēng)格。請(qǐng)求處理代碼不是從頭到尾處理一個(gè)線程上的請(qǐng)求,而是在等待I/O操作完成時(shí)將其線程返回到池,以便線程可以為其他請(qǐng)求提供服務(wù)。這種細(xì)粒度的線程共享–在這種共享中,代碼僅在線程執(zhí)行計(jì)算時(shí)保留線程,而不是在等待I/O時(shí)保留線程–允許大量并發(fā)操作,而不會(huì)消耗大量線程。雖然它消除了操作系統(tǒng)線程稀缺性對(duì)吞吐量的限制,但它的代價(jià)很高:它需要所謂的異步編程風(fēng)格,使用一組單獨(dú)的I/O方法,這些方法不等待I/O操作完成,而是,稍后,向回調(diào)表示它們的完成。如果沒(méi)有專用線程,開(kāi)發(fā)人員必須將其請(qǐng)求處理邏輯分解為小階段,通常編寫為lambda表達(dá)式,然后使用API將它們組合成順序管道(請(qǐng)參見(jiàn)CompletableFuture,或所謂的“反應(yīng)性”(reactive)框架。因此,它們放棄了語(yǔ)言的基本順序組成運(yùn)算符,如循環(huán)和try/catch塊。
在異步風(fēng)格中,請(qǐng)求的每個(gè)階段都可能在不同的線程上執(zhí)行,每個(gè)線程以交錯(cuò)的方式運(yùn)行屬于不同請(qǐng)求的階段。這對(duì)理解程序行為具有深刻的影響:堆棧跟蹤不提供可用的上下文,調(diào)試器無(wú)法逐步完成請(qǐng)求處理邏輯,分析器無(wú)法將操作的成本與其調(diào)用者關(guān)聯(lián)起來(lái)。在使用Java時(shí),編寫lambda表達(dá)式是可以管理的Stream API在短管道中處理數(shù)據(jù),但當(dāng)應(yīng)用程序中的所有請(qǐng)求處理代碼都必須以這種方式編寫時(shí),就會(huì)有問(wèn)題。這種編程風(fēng)格與Java平臺(tái)不一致,因?yàn)閼?yīng)用程序的并發(fā)單位–異步管道–不再是平臺(tái)的并發(fā)單位。
使用虛擬線程保留線程每請(qǐng)求樣式
為了使應(yīng)用程序能夠擴(kuò)展,同時(shí)與平臺(tái)保持和諧,我們應(yīng)該通過(guò)更有效地實(shí)現(xiàn)線程來(lái)努力保留每個(gè)請(qǐng)求的線程風(fēng)格,這樣它們就可以更豐富。操作系統(tǒng)無(wú)法更有效地實(shí)現(xiàn)操作系統(tǒng)線程,因?yàn)椴煌恼Z(yǔ)言和運(yùn)行時(shí)以不同的方式使用線程堆棧。但是,Java運(yùn)行時(shí)可以以一種將Java線程與操作系統(tǒng)線程的一對(duì)一對(duì)應(yīng)關(guān)系分開(kāi)的方式實(shí)現(xiàn)Java線程。就像操作系統(tǒng)通過(guò)將大量虛擬地址空間映射到有限數(shù)量的物理RAM來(lái)給人豐富內(nèi)存的錯(cuò)覺(jué)一樣,Java運(yùn)行時(shí)也可以通過(guò)將大量虛擬線程映射到少量操作系統(tǒng)線程來(lái)給人豐富線程的錯(cuò)覺(jué)。
虛擬線程是java.lang.Thread的一個(gè)實(shí)例,它不綁定到特定的操作系統(tǒng)線程。相比之下,平臺(tái)線程是java.lang.Thread的實(shí)例,以傳統(tǒng)方式實(shí)現(xiàn),作為操作系統(tǒng)線程周圍的精簡(jiǎn)包裝器。
線程每請(qǐng)求樣式中的應(yīng)用程序代碼可以在虛擬線程中運(yùn)行整個(gè)請(qǐng)求持續(xù)時(shí)間,但虛擬線程僅在CPU上執(zhí)行計(jì)算時(shí)消耗操作系統(tǒng)線程。結(jié)果是與異步風(fēng)格相同的可擴(kuò)展性,只是它是透明的:當(dāng)在虛擬線程中運(yùn)行的代碼調(diào)用java中的阻塞I/O操作的java.* API時(shí),運(yùn)行時(shí)執(zhí)行非阻塞操作系統(tǒng)調(diào)用,并自動(dòng)掛起虛擬線程,直到稍后可以恢復(fù)。對(duì)于Java開(kāi)發(fā)人員來(lái)說(shuō),虛擬線程只是創(chuàng)建起來(lái)很便宜,而且?guī)缀鯚o(wú)限豐富的線程。硬件利用率接近最佳,允許高水平的并發(fā)性,從而實(shí)現(xiàn)高吞吐量,同時(shí)應(yīng)用程序與Java平臺(tái)及其工具的多線程設(shè)計(jì)保持和諧。
虛擬線程的含義
虛擬線程既便宜又豐富,因此永遠(yuǎn)不應(yīng)該池化:應(yīng)該為每個(gè)應(yīng)用程序任務(wù)創(chuàng)建一個(gè)新的虛擬線程。因此,大多數(shù)虛擬線程都是短暫的,并且具有淺調(diào)用堆棧,執(zhí)行的時(shí)間只需單個(gè)HTTP客戶端調(diào)用或單個(gè)JDBC查詢。相比之下,平臺(tái)線程是重量級(jí)和昂貴的,因此通常必須池化。它們往往是長(zhǎng)壽命的,具有深度的調(diào)用堆棧,并在許多任務(wù)中共享。
總之,虛擬線程保留了可靠的每請(qǐng)求線程風(fēng)格,該風(fēng)格與Java平臺(tái)的設(shè)計(jì)和諧,同時(shí)優(yōu)化地利用硬件。使用虛擬線程不需要學(xué)習(xí)新的概念,盡管它可能需要養(yǎng)成不學(xué)習(xí)的習(xí)慣,以應(yīng)對(duì)當(dāng)今線程的高成本。虛擬線程不僅將幫助應(yīng)用程序開(kāi)發(fā)人員,還將幫助框架設(shè)計(jì)人員提供易于使用的API,這些API與平臺(tái)的設(shè)計(jì)兼容,而不影響可擴(kuò)展性。
描述
今天,每一個(gè)例子 java.lang.Thread在JDK中,是一個(gè)平臺(tái)線程。平臺(tái)線程在底層操作系統(tǒng)線程上運(yùn)行Java代碼,并在代碼的整個(gè)生命周期內(nèi)捕獲操作系統(tǒng)線程。平臺(tái)線程數(shù)限制為操作系統(tǒng)線程數(shù)。
虛擬線程是java.lang.Thread的一個(gè)實(shí)例,它在基礎(chǔ)操作系統(tǒng)線程上運(yùn)行Java代碼,但在代碼的整個(gè)生命周期內(nèi)不捕獲操作系統(tǒng)線程。這意味著許多虛擬線程可以在同一操作系統(tǒng)線程上運(yùn)行其Java代碼,有效地共享它。雖然平臺(tái)線程壟斷了寶貴的操作系統(tǒng)線程,但虛擬線程卻不壟斷。虛擬線程的數(shù)量可以遠(yuǎn)大于操作系統(tǒng)線程的數(shù)量。
虛擬線程是由JDK而不是操作系統(tǒng)提供的線程的輕量級(jí)實(shí)現(xiàn)。它們是用戶模式線程的一種形式,在其他多線程語(yǔ)言中已經(jīng)成功(例如,Go中的goroutine和Erlang中的進(jìn)程)。用戶態(tài)線程甚至被稱為“green threads”在Java的早期版本中,當(dāng)時(shí)操作系統(tǒng)線程還不成熟和廣泛。然而,Java的green threads都共享一個(gè)操作系統(tǒng)線程(M:1調(diào)度),平臺(tái)線程被實(shí)現(xiàn)為操作系統(tǒng)線程的包裝器(1:1調(diào)度)。虛擬線程采用M:N調(diào)度,其中大量(M)虛擬線程被調(diào)度在較少數(shù)量(N)的操作系統(tǒng)線程上運(yùn)行。
使用虛擬線程vs平臺(tái)線程
開(kāi)發(fā)者可以選擇使用虛擬線程還是平臺(tái)線程。這里是一個(gè)創(chuàng)建大量虛擬線程的示例程序。程序首先獲取一個(gè)ExecutorService這將為每個(gè)提交的任務(wù)創(chuàng)建一個(gè)新的虛擬線程。然后,它提交10,000個(gè)任務(wù),并等待所有任務(wù)完成:
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
IntStream.range(0, 10_000).forEach(i -> {
executor.submit(() -> {
Thread.sleep(Duration.ofSeconds(1));
return i;
});
});
} // executor.close() is called implicitly, and waits
本例中的任務(wù)是簡(jiǎn)單的代碼–睡眠一秒鐘–現(xiàn)代硬件可以輕松支持10000個(gè)虛擬線程同時(shí)運(yùn)行此類代碼。在幕后,JDK在少量的操作系統(tǒng)線程上運(yùn)行代碼,也許只有一個(gè)線程。
如果此程序使用為每個(gè)任務(wù)創(chuàng)建新的平臺(tái)線程的ExecutorService,如Executor.newCachedThreadPool(),情況就會(huì)大不相同。ExecutorService將嘗試創(chuàng)建10000個(gè)平臺(tái)線程,從而創(chuàng)建10000個(gè)操作系統(tǒng)線程,并且該程序?qū)⒃诖蠖鄶?shù)操作系統(tǒng)上崩潰。
相反,如果程序使用從池獲取平臺(tái)線程的ExecutorService,如Executor.newFixedThreadPool(200),情況也不會(huì)好太多。ExecutorService將創(chuàng)建200個(gè)平臺(tái)線程,供所有10000個(gè)任務(wù)共享,因此許多任務(wù)將順序運(yùn)行,而不是并發(fā)運(yùn)行,程序?qū)⑿枰荛L(zhǎng)時(shí)間才能完成。對(duì)于此程序,具有200個(gè)平臺(tái)線程的池只能實(shí)現(xiàn)每秒200個(gè)任務(wù)的吞吐量,而虛擬線程的吞吐量則約為每秒10000個(gè)任務(wù)(在充分預(yù)熱后)。此外,如果示例程序中的10_000更改為1_000_000,則程序?qū)⑻峤?00萬(wàn)個(gè)任務(wù),創(chuàng)建100萬(wàn)個(gè)同時(shí)運(yùn)行的虛擬線程,并(在充分預(yù)熱后)實(shí)現(xiàn)每秒約100萬(wàn)個(gè)任務(wù)的吞吐量。
如果此程序中的任務(wù)執(zhí)行了一秒鐘的計(jì)算(例如,排序一個(gè)巨大的數(shù)組),而不僅僅是睡眠,那么將線程數(shù)量增加到處理器核心數(shù)量之外都沒(méi)有幫助,不管它們是虛擬線程還是平臺(tái)線程。虛擬線程不是更快的線程-它們運(yùn)行代碼的速度并不比平臺(tái)線程快。它們的存在是為了提供規(guī)模(更高的吞吐量),而不是速度(更低的延遲)。它們可能比平臺(tái)線程多得多,因此根據(jù)利特爾定律,它們實(shí)現(xiàn)了更高吞吐量所需的更高并發(fā)。
換一種方式說(shuō),虛擬線程可以顯著提高應(yīng)用程序吞吐量,當(dāng)
- 并發(fā)任務(wù)數(shù)量較高(超過(guò)幾千個(gè)),并且
- 工作負(fù)載不綁定CPU,因?yàn)樵谶@種情況下,線程數(shù)比處理器內(nèi)核多得多不能提高吞吐量。
虛擬線程有助于提高典型服務(wù)器應(yīng)用程序的吞吐量,正是因?yàn)榇祟悜?yīng)用程序由大量并發(fā)任務(wù)組成,這些任務(wù)花費(fèi)了大量時(shí)間等待。
虛擬線程可以運(yùn)行平臺(tái)線程可以運(yùn)行的任何代碼。特別是,虛擬線程支持線程局部變量和線程中斷,就像平臺(tái)線程一樣。這意味著處理請(qǐng)求的現(xiàn)有Java代碼將很容易在虛擬線程中運(yùn)行。許多服務(wù)器框架將選擇自動(dòng)執(zhí)行此操作,為每個(gè)傳入請(qǐng)求啟動(dòng)一個(gè)新的虛擬線程,并在其中運(yùn)行應(yīng)用程序的業(yè)務(wù)邏輯。
以下是一個(gè)服務(wù)器應(yīng)用程序的示例,它聚合了其他兩個(gè)服務(wù)的結(jié)果。假設(shè)的服務(wù)器框架(未顯示)為每個(gè)請(qǐng)求創(chuàng)建一個(gè)新的虛擬線程,并在該虛擬線程中運(yùn)行應(yīng)用程序的句柄代碼。應(yīng)用程序代碼反過(guò)來(lái)創(chuàng)建兩個(gè)新的虛擬線程,通過(guò)與第一個(gè)示例相同的ExecutorService并發(fā)獲取資源:
void handle(Request request, Response response) {
var url1 = ...
var url2 = ...
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
var future1 = executor.submit(() -> fetchURL(url1));
var future2 = executor.submit(() -> fetchURL(url2));
response.send(future1.get() + future2.get());
} catch (ExecutionException | InterruptedException e) {
response.fail(e);
}
}
String fetchURL(URL url) throws IOException {
try (var in = url.openStream()) {
return new String(in.readAllBytes(), StandardCharsets.UTF_8);
}
}
像這樣的服務(wù)器應(yīng)用程序,具有簡(jiǎn)單的阻塞代碼,可以很好地?cái)U(kuò)展,因?yàn)樗梢允褂么罅康奶摂M線程。 Executor.newVirtualThreadPerTaskExecutor()并不是創(chuàng)建虛擬線程的唯一方法。新的java.lang.Thread.BuilderAPI,下面討論,可以創(chuàng)建和啟動(dòng)虛擬線程。此外,結(jié)構(gòu)化并發(fā)提供了一個(gè)更強(qiáng)大的API來(lái)創(chuàng)建和管理虛擬線程,特別是在類似于此服務(wù)器示例的代碼中,通過(guò)該API,線程之間的關(guān)系將被平臺(tái)及其工具所知道。
虛擬線程是一個(gè)預(yù)覽API,默認(rèn)禁用
上面的程序使用Executors.newVirtualThreadPerTaskExecutor()方法,因此要在JDK XX上運(yùn)行它們,必須按以下方式啟用預(yù)覽API:
- 使用
javac --release XX --enable-preview Main.java編譯程序,并使用java--enable-preview Main運(yùn)行程序;或, - 當(dāng)使用源代碼啟動(dòng)器,使用
java --release XX --enable-preview Main.java;運(yùn)行程序;或, - 使用時(shí)jshell,以
jshell --enable-preview開(kāi)頭
不池化虛擬線程
開(kāi)發(fā)人員通常會(huì)將應(yīng)用程序代碼從基于線程池的傳統(tǒng)ExecutorService遷移到虛擬線程每任務(wù)ExecutorService。線程池和所有資源池一樣,旨在共享昂貴的資源,但虛擬線程并不昂貴,而且永遠(yuǎn)不需要將它們池化。
開(kāi)發(fā)人員有時(shí)使用線程池來(lái)限制對(duì)有限資源的并發(fā)訪問(wèn)。例如,如果一個(gè)服務(wù)不能處理超過(guò)20個(gè)并發(fā)請(qǐng)求,則通過(guò)提交到大小為20的池的任務(wù)執(zhí)行對(duì)該服務(wù)的所有訪問(wèn)將確保這一點(diǎn)。由于平臺(tái)線程的高成本使線程池?zé)o處不在,這種成語(yǔ)也變得無(wú)處不在,但開(kāi)發(fā)人員不應(yīng)該被誘惑池虛擬線程以限制并發(fā)。專門為此目的設(shè)計(jì)的構(gòu)造,如信號(hào)量,應(yīng)用于保護(hù)對(duì)有限資源的訪問(wèn)。這比線程池更有效和方便,也更安全,因?yàn)榫€程本地?cái)?shù)據(jù)不會(huì)意外從一個(gè)任務(wù)泄露到另一個(gè)任務(wù)的風(fēng)險(xiǎn)。
觀察虛擬線程
編寫清晰的代碼并不是完整的故事。清楚地表示正在運(yùn)行的程序的狀態(tài)對(duì)于故障排除、維護(hù)和優(yōu)化也是必不可少的,JDK長(zhǎng)期以來(lái)一直提供調(diào)試、分析和監(jiān)控線程的機(jī)制。這些工具應(yīng)該對(duì)虛擬線程執(zhí)行同樣的操作–也許可以對(duì)其大量的操作進(jìn)行一些調(diào)節(jié)–因?yàn)樗鼈儺吘故莏ava.lang.Thread的實(shí)例。
Java調(diào)試器可以逐步瀏覽虛擬線程、顯示調(diào)用堆棧和檢查堆棧幀中的變量。JDK Flight Recorder (JFR)是JDK的低開(kāi)銷分析和監(jiān)控機(jī)制,可以將應(yīng)用程序代碼中的事件(如對(duì)象分配和I/O操作)與正確的虛擬線程關(guān)聯(lián)。這些工具不能為以異步樣式編寫的應(yīng)用程序執(zhí)行這些操作。在這種風(fēng)格中,任務(wù)與線程無(wú)關(guān),因此調(diào)試器無(wú)法顯示或操作任務(wù)的狀態(tài),分析器也無(wú)法判斷任務(wù)等待I/O的時(shí)間。
線程轉(zhuǎn)儲(chǔ)是另一個(gè)流行的工具,用于對(duì)以線程每請(qǐng)求風(fēng)格編寫的應(yīng)用程序進(jìn)行故障排除。不幸的是,JDK的傳統(tǒng)線程轉(zhuǎn)儲(chǔ),使用jstack或jcmd獲得,提供了一個(gè)線程的扁平列表。這適合幾十個(gè)或數(shù)百個(gè)平臺(tái)線程,但不適合數(shù)千個(gè)或數(shù)百萬(wàn)個(gè)虛擬線程。因此,我們不會(huì)擴(kuò)展傳統(tǒng)的線程轉(zhuǎn)儲(chǔ)以包括虛擬線程,而是在jcmd中引入一種新的線程轉(zhuǎn)儲(chǔ),以將虛擬線程與平臺(tái)線程一起呈現(xiàn),所有這些線程都以有意義的方式分組。當(dāng)程序使用時(shí),可以顯示出線程之間更豐富的關(guān)系結(jié)構(gòu)化并發(fā)。
由于可視化和分析大量線程可以從工具中受益,jcmd除了純文本外,還可以以JSON格式發(fā)出新的線程轉(zhuǎn)儲(chǔ):
$ jcmd <pid> Thread.dump_to_file -format=json <file>
新的線程轉(zhuǎn)儲(chǔ)格式列出了在網(wǎng)絡(luò)I/O操作中被阻止的虛擬線程,以及由上面所示的new-thread-per-task(一任務(wù)一線程)ExecutorService創(chuàng)建的虛擬線程。它不包括對(duì)象地址、鎖、JNI統(tǒng)計(jì)信息、堆統(tǒng)計(jì)信息和傳統(tǒng)線程轉(zhuǎn)儲(chǔ)中出現(xiàn)的其他信息。此外,由于它可能需要列出大量線程,因此生成新的線程轉(zhuǎn)儲(chǔ)不會(huì)暫停應(yīng)用程序。 以下是這樣的線程轉(zhuǎn)儲(chǔ)的示例,取自與上面第二個(gè)示例類似的應(yīng)用程序,在JSON查看器中呈現(xiàn)(見(jiàn)下圖):
[圖片上傳失敗...(image-3e375a-1695296921208)]
由于虛擬線程是在JDK中實(shí)現(xiàn)的,并且不綁定到任何特定的操作系統(tǒng)線程,因此它們對(duì)操作系統(tǒng)是不可見(jiàn)的,而操作系統(tǒng)不知道它們的存在。操作系統(tǒng)級(jí)監(jiān)控將觀察到JDK進(jìn)程使用的操作系統(tǒng)線程比虛擬線程少。
調(diào)度虛擬線程
要做有用的工作,需要調(diào)度線程,即分配在處理器核心上執(zhí)行。對(duì)于作為操作系統(tǒng)線程實(shí)現(xiàn)的平臺(tái)線程,JDK依賴于操作系統(tǒng)中的調(diào)度程序。相比之下,對(duì)于虛擬線程,JDK有自己的調(diào)度程序。JDK的調(diào)度程序?qū)⑻摂M線程分配給平臺(tái)線程,而不是直接將虛擬線程分配給處理器(這就是前面提到的虛擬線程的M:N調(diào)度)。然后,操作系統(tǒng)將像往常一樣調(diào)度平臺(tái)線程。
JDK的虛擬線程調(diào)度程序是一個(gè)竊取工作的工具ForkJoinPool在FIFO模式下工作。調(diào)度程序的并行性是可用于調(diào)度虛擬線程的平臺(tái)線程數(shù)。默認(rèn)情況下,它等于可用處理器,但它可以使用系統(tǒng)屬性jdk.virtualThreadScheduler.parallelism進(jìn)行調(diào)整。請(qǐng)注意,此ForkJoinPool不同于公共池,例如,它用于并行流的實(shí)現(xiàn),并在后進(jìn)先出模式下工作。
調(diào)度程序分配虛擬線程的平臺(tái)線程稱為虛擬線程的載體。虛擬線程可以在其生命周期內(nèi)在不同的載體上調(diào)度;換句話說(shuō),調(diào)度程序不保持虛擬線程和任何特定平臺(tái)線程之間的親和性。但是,從Java代碼的角度來(lái)看,正在運(yùn)行的虛擬線程在邏輯上獨(dú)立于其當(dāng)前載體:
- 虛擬線程無(wú)法使用運(yùn)營(yíng)商的身份。Thread.currentThread()返回的值始終是虛擬線程本身。
- 載體和虛擬線程的堆棧跟蹤是分開(kāi)的。虛擬線程中拋出的異常將不包括載體的堆棧幀。線程轉(zhuǎn)儲(chǔ)不會(huì)在虛擬線程的堆棧中顯示載體的堆棧幀,反之亦然。
- 載體的線程局部變量對(duì)虛擬線程不可用,反之亦然。
此外,從Java代碼的角度來(lái)看,虛擬線程及其載體暫時(shí)共享操作系統(tǒng)線程的事實(shí)是不可見(jiàn)的。相比之下,從本機(jī)代碼的角度來(lái)看,虛擬線程和它的載體都運(yùn)行在同一個(gè)本機(jī)線程上。因此,在同一虛擬線程上多次調(diào)用的本機(jī)代碼可能會(huì)在每次調(diào)用時(shí)觀察到不同的操作系統(tǒng)線程標(biāo)識(shí)符。
調(diào)度程序當(dāng)前不為虛擬線程實(shí)現(xiàn)分時(shí)。分時(shí)是對(duì)占用分配數(shù)量的CPU時(shí)間的線程的強(qiáng)制搶占。雖然分時(shí)使用幾百個(gè)平臺(tái)線程可以有效,但目前還不清楚分時(shí)使用一百萬(wàn)個(gè)虛擬線程是否會(huì)那么有效。
執(zhí)行虛擬線程
要利用虛擬線程,沒(méi)有必要重寫程序。虛擬線程不要求或期望應(yīng)用程序代碼顯式地將控制權(quán)交回調(diào)度程序;換句話說(shuō),虛擬線程是不合作的。用戶代碼不得假設(shè)虛擬線程如何或何時(shí)分配給平臺(tái)線程,就如假設(shè)平臺(tái)線程如何或何時(shí)分配給處理器內(nèi)核一樣。
要在虛擬線程中運(yùn)行代碼,JDK的虛擬線程調(diào)度程序通過(guò)將虛擬線程掛載在平臺(tái)線程上,分配虛擬線程在平臺(tái)線程上執(zhí)行。這使得平臺(tái)線程成為虛擬線程的載體。稍后,在運(yùn)行一些代碼后,虛擬線程可以從其載體上卸載。在這一點(diǎn)上,平臺(tái)線程是空閑的,因此調(diào)度程序可以在其上裝載不同的虛擬線程,從而使其再次成為載體。
通常,當(dāng)虛擬線程阻塞I/O或JDK中的其他阻塞操作時(shí),虛擬線程將卸載,如BlockingQueue.take()。當(dāng)阻塞操作準(zhǔn)備完成時(shí)(例如,已在套接字上接收字節(jié)),它將虛擬線程提交回調(diào)度程序,調(diào)度程序?qū)⒃谳d體上裝載虛擬線程以恢復(fù)執(zhí)行。
虛擬線程的裝載和卸載頻繁且透明地進(jìn)行,并且不會(huì)阻止任何操作系統(tǒng)線程。例如,前面顯示的服務(wù)器應(yīng)用程序包括以下代碼行,其中包含對(duì)阻止操作的調(diào)用:
response.send(future1.get() + future2.get());
這些操作將導(dǎo)致虛擬線程多次裝載和卸載,通常每次調(diào)用get()一次,在send(…)中執(zhí)行I/O過(guò)程中可能多次。
JDK中的絕大多數(shù)阻塞操作將卸載虛擬線程,釋放其載體和底層操作系統(tǒng)線程來(lái)承擔(dān)新的工作。但是,JDK中的一些阻塞操作不會(huì)卸載虛擬線程,因此會(huì)阻塞其載體和底層操作系統(tǒng)線程。這是因?yàn)椴僮飨到y(tǒng)級(jí)別(例如,許多文件系統(tǒng)操作)或JDK級(jí)別(例如,Object.wait())。這些阻塞操作的實(shí)現(xiàn)將通過(guò)臨時(shí)擴(kuò)展調(diào)度程序的并行性來(lái)補(bǔ)償操作系統(tǒng)線程的捕獲。因此,調(diào)度程序的ForkJoinPool中的平臺(tái)線程數(shù)量可能暫時(shí)超過(guò)可用處理器的數(shù)量。調(diào)度程序可用的最大平臺(tái)線程數(shù)可以使用系統(tǒng)屬性jdk.virtualThreadScheduler.maxPoolSize進(jìn)行調(diào)整。
在兩種情況下,虛擬線程在阻塞操作期間無(wú)法卸載,因?yàn)樗还潭ㄔ谄漭d體上:
- 當(dāng)它在同步塊或方法中執(zhí)行代碼時(shí),或
- 當(dāng)它執(zhí)行本機(jī)方法或外來(lái)函數(shù).
固定不會(huì)使應(yīng)用程序不正確,但可能會(huì)妨礙其可擴(kuò)展性。如果虛擬線程在被固定時(shí)執(zhí)行阻塞操作,如I/O或BlockingQueue.take(),則在操作期間,其載體和基礎(chǔ)操作系統(tǒng)線程將被阻塞。長(zhǎng)時(shí)間頻繁固定可能會(huì)通過(guò)捕獲運(yùn)營(yíng)商而損害應(yīng)用程序的可擴(kuò)展性。
調(diào)度程序不會(huì)通過(guò)擴(kuò)展其并行度來(lái)補(bǔ)償固定。相反,通過(guò)修改頻繁運(yùn)行的同步塊或方法,并保護(hù)要使用的潛在長(zhǎng)I/O操作,避免頻繁和長(zhǎng)期的固定java.util.concurrent.locks.ReentrantLock相反,不需要替換不經(jīng)常使用的同步塊和方法(例如,僅在啟動(dòng)時(shí)執(zhí)行)或保護(hù)內(nèi)存操作。和往常一樣,努力保持鎖定策略簡(jiǎn)單明了。
新的診斷有助于將代碼遷移到虛擬線程,并評(píng)估是否應(yīng)將同步的特定使用替換為java.util.concurrent鎖:
- 當(dāng)線程在被固定時(shí)阻塞時(shí),將發(fā)出JDK Flight Recorder (JFR) 事件.
- 系統(tǒng)屬性
jdk.tracePinnedThreads在線程被固定時(shí)阻塞時(shí)觸發(fā)堆棧跟蹤。使用-Djdk.tracePinnedThreads=full運(yùn)行時(shí),當(dāng)線程在固定時(shí)阻塞時(shí),打印完整的堆棧跟蹤,本機(jī)幀和持有監(jiān)視器的幀高亮顯示。使用-Djdk.tracePinnedThreads=short將輸出限制為僅有問(wèn)題的幀。
我們也許能夠在未來(lái)的版本中刪除上面的第一個(gè)限制。第二個(gè)限制是與本機(jī)代碼正確交互所必需的。
內(nèi)存使用和與垃圾回收的交互
虛擬線程的堆棧作為堆棧塊對(duì)象存儲(chǔ)在Java的垃圾收集堆中。堆棧隨著應(yīng)用程序運(yùn)行而增長(zhǎng)和收縮,既是為了提高內(nèi)存效率,又是為了容納任意深度的堆棧(最高可達(dá)JVM配置的平臺(tái)線程堆棧大?。?。這種效率使大量虛擬線程得以實(shí)現(xiàn),從而使服務(wù)器應(yīng)用程序中線程每請(qǐng)求風(fēng)格的持續(xù)生存能力得以實(shí)現(xiàn)。
在上面的第二個(gè)示例中,請(qǐng)記住,假設(shè)框架通過(guò)創(chuàng)建新的虛擬線程并調(diào)用句柄方法來(lái)處理每個(gè)請(qǐng)求;即使它在深度調(diào)用堆棧的末尾調(diào)用句柄(在身份驗(yàn)證、事務(wù)等之后),句柄本身會(huì)生成多個(gè)僅執(zhí)行短期任務(wù)的虛擬線程。因此,對(duì)于每個(gè)具有深調(diào)用棧的虛擬線程,將有多個(gè)具有淺調(diào)用棧的虛擬線程消耗很少的內(nèi)存。
一般來(lái)說(shuō),虛擬線程所需的堆空間和垃圾收集器活動(dòng)量很難與異步代碼相比。處理請(qǐng)求的應(yīng)用程序代碼通常必須跨I/O操作維護(hù)數(shù)據(jù);線程每請(qǐng)求代碼可以將數(shù)據(jù)保留在局部變量中,局部變量存儲(chǔ)在堆中的虛擬線程堆棧上。而異步代碼必須在堆對(duì)象中保留相同數(shù)據(jù),這些數(shù)據(jù)從管道的一個(gè)階段傳遞到下一個(gè)階段。一方面,虛擬線程所需的堆棧幀布局比緊湊對(duì)象更浪費(fèi);另一方面,虛擬線程在許多情況下(取決于低級(jí)GC交互)可以變異和重用其堆棧,而異步管道總是需要分配新對(duì)象??傮w而言,線程每請(qǐng)求與異步代碼的堆消耗和垃圾收集器活動(dòng)應(yīng)該大致相似。隨著時(shí)間的推移,我們希望使虛擬線程堆棧的內(nèi)部表示更加緊湊。
與平臺(tái)線程棧不同,虛擬線程棧不是GC根,因此其中包含的引用不會(huì)被執(zhí)行并發(fā)掃描的垃圾收集器在停止世界暫停中遍歷。這也意味著,如果虛擬線程在上被阻塞,例如 BlockingQueue.take(),并且沒(méi)有其他線程可以獲得對(duì)虛擬線程或隊(duì)列的引用,那么該線程可以被垃圾收集-這很好,因?yàn)樘摂M線程永遠(yuǎn)不會(huì)被中斷或解除阻塞。當(dāng)然,如果虛擬線程正在運(yùn)行,或者如果它被阻止,并且可能會(huì)被取消阻止,則它不會(huì)被垃圾收集。
虛擬線程的當(dāng)前限制是G1 GC不支持巨大的堆棧塊對(duì)象。如果虛擬線程的堆棧達(dá)到區(qū)域大小的一半(通常為512KB),則可能會(huì)引發(fā)StackOverflowError。
詳細(xì)更改
其余小節(jié)詳細(xì)介紹了在Java平臺(tái)及其實(shí)現(xiàn)中提出的更改:
- java.lang.Thread
- Thread-local variables
- java.util.concurrent
- Networking
- java.io
- Java Native Interface (JNI)
- Debugging (JVM TI, JDWP, and JDI)
- JDK Flight Recorder (JFR)
- Java Management Extensions (JMX)
- java.lang.ThreadGroup
java.lang.Thread
更新 java.lang.Thread API如下:
- Thread.Builder、Thread.ofVirtual()和Thread.ofPlatform()是用于創(chuàng)建虛擬線程和平臺(tái)線程的新API。例如,
Thread thread = Thread.ofVirtual().name("duke").unstarted(runnable);創(chuàng)建一個(gè)名為“duke”的新未啟動(dòng)虛擬線程。 -
Thread.startVirtualThread(Runnable)是創(chuàng)建和啟動(dòng)虛擬線程的方便方法。 - Thread.Builder可以創(chuàng)建一個(gè)線程,也可以創(chuàng)建一個(gè)ThreadFactory,然后可以創(chuàng)建具有相同屬性的多個(gè)線程。
- Thread.isVirtual()測(cè)試線程是否為虛擬線程。
- Thread.join 和 Thread.sleep接受等待和睡眠時(shí)間作為java.time.Duration.
- 新的最終方法Thread.threadId() 返回線程的標(biāo)識(shí)符。現(xiàn)有非最終方法 Thread.getId() 現(xiàn)在已棄用。
- Thread.getAllStackTraces() 現(xiàn)在返回所有平臺(tái)線程而不是所有線程的映射。
java.lang.Thread API在其他方面保持不變。線程類定義的構(gòu)造函數(shù)創(chuàng)建平臺(tái)線程,與前面一樣。沒(méi)有新的公共構(gòu)造函數(shù)。
虛擬線程和平臺(tái)線程之間的主要API區(qū)別是:
- 公共線程構(gòu)造函數(shù)無(wú)法創(chuàng)建虛擬線程。
- 虛擬線程始終是守護(hù)線程。Thread.setDaemon(boolean) 方法無(wú)法將虛擬線程更改為非守護(hù)線程。
- 虛擬線程的固定優(yōu)先級(jí)為 Thread.NORM_PRIORITY。 Thread.setPriority(int) 方法對(duì)虛擬線程沒(méi)有影響。此限制可能會(huì)在未來(lái)的版本中重新討論。
- 虛擬線程不是線程組的活動(dòng)成員。在虛擬線程上調(diào)用時(shí),Thread.getThreadGroup() 返回名為“VirtualThreads”的占位線程組。Thread.Builder API沒(méi)有定義設(shè)置虛擬線程線程的線程組的方法。
- 使用SecurityManager集運(yùn)行時(shí),虛擬線程沒(méi)有權(quán)限。
- 虛擬線程不支持 stop()、suspend()、 resume()方法。這些方法在虛擬線程上調(diào)用時(shí)引發(fā)異常。
線程局部變量
虛擬線程支持線程局部變量(ThreadLocal)和可繼承的線程局部變量(InheritableThreadLocal),就像平臺(tái)線程一樣,因此它們可以運(yùn)行使用線程本地程序的現(xiàn)有代碼。但是,由于虛擬線程可能非常多,請(qǐng)?jiān)谧屑?xì)考慮后使用線程本地。特別是,不要使用線程本地在線程池中共享同一線程的多個(gè)任務(wù)之間匯集昂貴的資源。虛擬線程不應(yīng)池化,因?yàn)槊總€(gè)線程在其生命周期內(nèi)只運(yùn)行一個(gè)任務(wù)。我們已經(jīng)從java.base模塊中刪除了線程本地的許多使用,以準(zhǔn)備虛擬線程,以減少在使用數(shù)百萬(wàn)線程運(yùn)行時(shí)的內(nèi)存占用。
此外:
- Thread.Builder API定義了創(chuàng)建線程時(shí)選擇退出線程本地的方法。它還定義了選擇退出繼承可繼承線程局部的初始值的方法。當(dāng)從不支持線程本地的線程調(diào)用時(shí),ThreadLocal.get() 返回初始值和線程本地集(T)拋出異常。
- 遺產(chǎn)上下文類加載器現(xiàn)在指定為像本地可繼承線程一樣工作。如果Thread.setContextClassLoader(ClassLoader)在不支持線程本地的線程上調(diào)用,然后引發(fā)異常。
作用域-局部變量對(duì)于某些用例,可能會(huì)被證明是線程本地的更好替代方案。
java.util.concurrent
支持鎖定的原始API。java.util.concurrent.LockSupport,現(xiàn)在支持虛擬線程:駐留虛擬線程釋放基礎(chǔ)載體線程以執(zhí)行其他工作,取消駐留虛擬線程計(jì)劃它繼續(xù)。對(duì)LockSupport的此更改使所有使用它的API(鎖、信號(hào)量、阻塞隊(duì)列等)在虛擬線程中調(diào)用時(shí)都能優(yōu)雅地停放。
此外:
- Executors.newThreadPerTaskExecutor(ThreadFactory) 和 Executors.newVirtualThreadPerTaskExecutor() 創(chuàng)建一個(gè)ExecutorService,為每個(gè)任務(wù)創(chuàng)建一個(gè)新線程。這些方法支持遷移和與使用線程池和ExecutorService的現(xiàn)有代碼的互操作性。
- 執(zhí)行器服務(wù)現(xiàn)在延伸可自動(dòng)關(guān)閉,因此允許此API與上面示例中所示的try-with-resource構(gòu)造一起使用。
- Future現(xiàn)在定義了獲取已完成任務(wù)的結(jié)果或異常以及獲取任務(wù)狀態(tài)的方法。結(jié)合起來(lái),這些添加使我們可以很容易地將Future對(duì)象用作流的元素,過(guò)濾Future流以查找已完成的任務(wù),然后映射以獲得結(jié)果流。這些方法也將有助于為結(jié)構(gòu)化并發(fā)。
網(wǎng)絡(luò)
java.net和java.nio.channels包中的網(wǎng)絡(luò)API的實(shí)現(xiàn)現(xiàn)在與虛擬線程一起工作:對(duì)虛擬線程的操作,該操作阻止建立網(wǎng)絡(luò)連接或從套接字讀取,釋放基礎(chǔ)載體線程以執(zhí)行其他工作。
為了允許中斷和取消,阻塞I/O方法定義為java.net.Socket、ServerSocket和DatagramSocket現(xiàn)在指定為可中斷的在虛擬線程中調(diào)用時(shí):中斷套接字上阻塞的虛擬線程將取消鎖定線程并關(guān)閉套接字。阻止這些類型套接字上的I/O操作,當(dāng)從 InterruptibleChannel一直是可中斷的,因此此更改將這些API在創(chuàng)建時(shí)的行為與從通道獲取時(shí)的行為對(duì)齊。
java.io
java.io包提供字節(jié)流和字符流的API。這些API的實(shí)現(xiàn)是高度同步的,需要進(jìn)行更改,以避免在虛擬線程中使用它們時(shí)固定。
作為背景,面向字節(jié)的輸入/輸出流沒(méi)有被指定為線程安全,也沒(méi)有指定當(dāng)線程在讀或?qū)懛椒ㄖ斜蛔枞麜r(shí)調(diào)用close()時(shí)的預(yù)期行為。在大多數(shù)情況下,使用來(lái)自多個(gè)并發(fā)線程的特定輸入或輸出流是沒(méi)有意義的。面向字符的讀取器/寫入器也沒(méi)有被指定為線程安全的,但它們確實(shí)公開(kāi)了子類的鎖對(duì)象。除了固定之外,這些類中的同步是有問(wèn)題的和不一致的;例如,InputStreamReader和OutputStreamWriter使用的流解碼器和編碼器在流對(duì)象上同步,而不是在鎖對(duì)象上同步。
為防止固定,實(shí)現(xiàn)現(xiàn)在的工作方式如下:
- BufferedInputStream、BufferedOutputStream、BufferedReader、BufferedWriter、PrintStream和PrintWriter現(xiàn)在在直接使用時(shí)使用顯式鎖而不是監(jiān)視器。這些類在子類時(shí)與以前一樣同步。
- InputStreamReader和OutputStreamWriter使用的流解碼器和編碼器現(xiàn)在使用與封閉的InputStreamReader或OutputStreamWriter相同的鎖。
更進(jìn)一步,消除所有這些通常不必要的鎖定超出了本JEP的范圍。
此外,BufferedOutputStream、BufferedWriter和OutputStreamWriter的流編碼器使用的緩沖區(qū)的初始大小現(xiàn)在更小,以便在堆中有許多流或?qū)懭氤绦驎r(shí)減少內(nèi)存使用-如果有是一百萬(wàn)個(gè)虛擬線程,每個(gè)線程在套接字連接上都有一個(gè)緩沖流。
Java Native Interface (JNI)
JNI定義了一個(gè)新函數(shù)IsVirtualThread,用于測(cè)試對(duì)象是否為虛擬線程。
JNI規(guī)范在其他方面保持不變。
調(diào)試
調(diào)試體系結(jié)構(gòu)由三個(gè)接口組成:JVM Tool Interface (JVM TI)、Java Debug Wire Protocol (JDWP)和Java Debug Interface (JDI)?,F(xiàn)在,所有三個(gè)接口都支持虛擬線程。
更新到JVM TI是:
- 大多數(shù)用jthread 調(diào)用的函數(shù)(即,對(duì)線程對(duì)象的JNI引用)都可以用對(duì)虛擬線程的引用調(diào)用。虛擬線程不支持少量函數(shù),即PopFrame、ForceEarlyReturn、StopThread、AgentStartFunction和GetThreadCpuTime。
SetLocal*函數(shù)僅限于在斷點(diǎn)或單步事件掛起的虛擬線程的最頂層幀中設(shè)置局部變量。 - 現(xiàn)在,GetAllThreads和GetAllStackTraces函數(shù)被指定為返回所有平臺(tái)線程,而不是所有線程。
- 除在早期VM啟動(dòng)或堆迭代期間發(fā)布的事件外,所有事件都可以在虛擬線程的上下文中調(diào)用事件回調(diào)。
- 掛起/恢復(fù)實(shí)現(xiàn)允許調(diào)試器掛起和恢復(fù)虛擬線程,并允許在掛起虛擬線程時(shí)掛起載體線程。
- 一項(xiàng)新功能can_support_virtual_threads,使代理可以更精細(xì)地控制虛擬線程的線程開(kāi)始和結(jié)束事件。
- 新函數(shù)支持虛擬線程的批量掛起和恢復(fù);這些函數(shù)需要can_support_virtual_threads功能。
現(xiàn)有的JVM TI代理將與以前一樣工作,但如果它們調(diào)用虛擬線程不支持的函數(shù),則可能會(huì)遇到錯(cuò)誤。當(dāng)不知道虛擬線程的代理與使用虛擬線程的應(yīng)用程序一起使用時(shí),就會(huì)出現(xiàn)這些情況。對(duì)某些代理來(lái)說(shuō),更改GetAllThreads以返回僅包含平臺(tái)線程的數(shù)組可能是一個(gè)問(wèn)題。啟用ThreadStart和ThreadEnd事件的現(xiàn)有代理可能會(huì)遇到性能問(wèn)題,因?yàn)樗鼈儫o(wú)法將這些事件限制在平臺(tái)線程上。
更新到JDWP是:
- 一個(gè)新命令允許調(diào)試器測(cè)試線程是否為虛擬線程。
- EventRequest命令上的新修飾符允許調(diào)試器將線程開(kāi)始和結(jié)束事件限制在平臺(tái)線程上。
更新到JDI是:
- 一種新的方法com.sun.jdi.ThreadReference測(cè)試線程是否為虛擬線程。
- 中的新方法com.sun.jdi.request.ThreadStartRequest和com.sun.jdi.request.ThreadDeathRequest將為請(qǐng)求生成的事件限制為平臺(tái)線程。
如上所述,虛擬線程不被視為線程組中的活動(dòng)線程。因此,JVM TI函數(shù)GetThreadGroupCalt、JDWP命令ThreadGroupReference/Children和JDI方法 com.sun.jdi.ThreadGroupReference.threads() 返回的線程列表僅包括平臺(tái)線程。
JDK Flight Recorder (JFR)
JFR支持具有多個(gè)新事件的虛擬線程:
- jdk.VirtualThreadStart和jdk.VirtualThreadEnd表示虛擬線程的開(kāi)始和結(jié)束。默認(rèn)情況下,這些事件是禁用的。
- jdk.VirtualThreadPinned表示虛擬線程在被固定時(shí)被駐留,即,沒(méi)有釋放其載體線程(參見(jiàn)限制)。此事件默認(rèn)啟用,閾值為20ms。
- jdk.VirtualThreadSubmitFailed表示啟動(dòng)或取消駐留虛擬線程失敗,可能是由于資源問(wèn)題。默認(rèn)情況下,此事件處于啟用狀態(tài)。
Java Management Extensions (JMX)
一種新的方法com.sun.management.HotSpotDiagnosticsMXBean 生成描述的新式線程轉(zhuǎn)儲(chǔ)以上。該方法也可以通過(guò)平臺(tái)間接調(diào)用 MBeanServer從本地或遠(yuǎn)程JMX工具。
java.lang.management.ThreadMXBean 僅支持平臺(tái)線程的監(jiān)控和管理。
java.lang.ThreadGroup
java.lang.ThreadGroup是一個(gè)用于分組線程的舊API,在現(xiàn)代應(yīng)用程序中很少使用,不適合分組虛擬線程。我們現(xiàn)在不推薦并降級(jí)它,并希望在未來(lái)引入一個(gè)新的線程組織結(jié)構(gòu),作為結(jié)構(gòu)化并發(fā)。
作為背景,ThreadGroup API可以追溯到Java 1.0。它最初旨在提供作業(yè)控制操作,如停止組中的所有線程?,F(xiàn)代代碼更傾向于使用java.util.concurrent的線程池API(在Java 5中引入)。ThreadGroup在早期Java版本中支持小程序的隔離,但Java安全體系結(jié)構(gòu)在Java 1.2中發(fā)生了顯著的發(fā)展,ThreadGroup不再發(fā)揮重要作用。ThreadGroup也旨在用于診斷目的,但該角色已被Java 5中引入的監(jiān)控和管理功能所取代,包括java.lang.管理API. 除了現(xiàn)在基本上不相關(guān)之外,ThreadGroup API和實(shí)現(xiàn)還存在許多重大問(wèn)題:
- 銷毀線程組的API和機(jī)制存在缺陷。
- API要求實(shí)現(xiàn)引用組中的所有活動(dòng)線程。這將為線程創(chuàng)建、線程啟動(dòng)和線程終止增加同步和競(jìng)爭(zhēng)開(kāi)銷。
- API定義了 enumerate() 方法,這些方法本身是有效的。
- API定義了suspend()、resume()和stop()方法,這些方法本身就容易死鎖和不安全。
現(xiàn)在指定、不建議使用和降級(jí)ThreadGroup ,如下所示:
- 刪除了顯式銷毀線程組的能力:最終不建議使用的destroy()方法什么都不做。
- 守護(hù)程序線程組的概念已刪除:守護(hù)程序狀態(tài)設(shè)置并由最終棄用的setDaemon(boolean) 和 isDaemon() 方法被忽略。
- 該實(shí)現(xiàn)不再保留對(duì)子組的強(qiáng)引用。當(dāng)線程組中沒(méi)有活動(dòng)線程,并且沒(méi)有其他任何其他線程保持線程組活動(dòng)時(shí),線程組現(xiàn)在有資格被垃圾收集。
- 最終不建議使用的suspend()、resume()和stop()方法總是拋出異常。