Java虛擬線程(Virtual Threads)

很多語言都有類似于“虛擬線程”的技術(shù)电湘,比如Go、C#鹅经、Erlang寂呛、Lua等,他們稱之為“協(xié)程”瘾晃。 不管是虛擬線程還是協(xié)程贷痪,他們都是輕量級線程,其目的都是為了提高并發(fā)能力蹦误。 本節(jié)詳細(xì)介紹Java平臺的“虛擬線程”的技術(shù)——“JEP 425: Virtual Threads (Preview)”劫拢。

Java平臺計劃引入虛擬線程,可顯著減少編寫强胰、維護(hù)和觀察高吞吐量并發(fā)應(yīng)用程序的工作量舱沧。“JEP 425: Virtual Threads (Preview)”目是一個預(yù)覽性的API偶洋。

目標(biāo)

  • 使以簡單的線程每請求風(fēng)格編寫的服務(wù)器應(yīng)用程序能夠以近乎最佳的硬件利用率進(jìn)行擴(kuò)展熟吏。
  • 啟用使用java.lang.Thread API的現(xiàn)有代碼,以最小的更改采用虛擬線程玄窝。
  • 使用現(xiàn)有的JDK工具分俯,輕松地對虛擬線程進(jìn)行故障排除肾筐、調(diào)試和分析。

非目標(biāo)

  • 目標(biāo)不是刪除線程的傳統(tǒng)實現(xiàn)缸剪,也不是靜默遷移現(xiàn)有應(yīng)用程序以使用虛擬線程吗铐。
  • 改變Java的基本并發(fā)模型并不是目標(biāo)。
  • 在Java語言或Java庫中提供新的數(shù)據(jù)并行結(jié)構(gòu)并不是目標(biāo)杏节。Stream API仍然是并行處理大型數(shù)據(jù)集的首選方式唬渗。

動機(jī)

近30年來,Java開發(fā)人員一直依賴線程作為并發(fā)服務(wù)器應(yīng)用程序的構(gòu)建塊奋渔。每個方法中的每個語句都在線程內(nèi)執(zhí)行镊逝,由于Java是多線程的,多個執(zhí)行線程同時發(fā)生嫉鲸。線程是Java的并發(fā)單元:一段順序代碼撑蒜,與其他此類單元同時運行,而且在很大程度上獨立于其他此類單元玄渗。每個線程都提供一個堆棧來存儲局部變量和協(xié)調(diào)方法調(diào)用座菠,以及出錯時的上下文:異常被同一線程中的方法拋出和捕獲,因此開發(fā)人員可以使用線程的堆棧跟蹤來查找發(fā)生了什么藤树。線程也是工具的核心概念:調(diào)試器逐步瀏覽線程方法中的語句浴滴,分析器可視化多個線程的行為,以幫助了解它們的性能岁钓。

線程每請求樣式

服務(wù)器應(yīng)用程序通常處理相互獨立的并發(fā)用戶請求升略,因此應(yīng)用程序通過在請求的整個持續(xù)時間內(nèi)將線程專用于該請求來處理請求是有意義的。這種線程每請求風(fēng)格易于理解屡限、易于編程品嚣、易于調(diào)試和分析,因為它使用平臺的并發(fā)單位來表示應(yīng)用程序的并發(fā)單位钧大。

服務(wù)器應(yīng)用程序的可擴(kuò)展性遵循利特爾定律(Little’s Law)腰根,它與延遲、并發(fā)和吞吐量有關(guān):對于給定的請求處理持續(xù)時間(即延遲)拓型,應(yīng)用程序同時處理的請求數(shù)(即额嘿,并發(fā))必須與到達(dá)速率(即吞吐量)成比例增長。例如劣挫,假設(shè)平均延遲為50ms的應(yīng)用程序通過同時處理10個請求册养,實現(xiàn)每秒200個請求的吞吐量。為了使該應(yīng)用程序擴(kuò)展到每秒2000個請求的吞吐量压固,它需要同時處理100個請求球拦。如果每個請求在請求的持續(xù)時間內(nèi)都在線程中處理,那么,要使應(yīng)用程序跟上坎炼,線程數(shù)量必須隨著吞吐量的增長而增長愧膀。

不幸的是,可用線程的數(shù)量是有限的谣光,因為JDK將線程作為操作系統(tǒng)(OS)線程的包裝器實現(xiàn)檩淋。操作系統(tǒng)線程成本高昂,因此我們不能擁有太多線程萄金,這使得實現(xiàn)不適合線程每請求風(fēng)格蟀悦。如果每個請求在其持續(xù)時間內(nèi)消耗一個線程,從而消耗一個操作系統(tǒng)線程氧敢,那么線程數(shù)量通常在其他資源(如CPU或網(wǎng)絡(luò)連接)耗盡之前很久就成為限制因素日戈。JDK當(dāng)前的線程實現(xiàn)將應(yīng)用程序的吞吐量限制在遠(yuǎn)低于硬件所能支持的水平。即使線程池化孙乖,也會發(fā)生這種情況浙炼,因為池化有助于避免啟動新線程的高成本,但不會增加線程總數(shù)唯袄。

使用異步風(fēng)格提高可擴(kuò)展性

一些希望充分利用硬件的開發(fā)人員放棄了每請求線程風(fēng)格弯屈,轉(zhuǎn)到線程共享風(fēng)格。請求處理代碼不是從頭到尾處理一個線程上的請求越妈,而是在等待I/O操作完成時將其線程返回到池季俩,以便線程可以為其他請求提供服務(wù)钮糖。這種細(xì)粒度的線程共享–在這種共享中梅掠,代碼僅在線程執(zhí)行計算時保留線程,而不是在等待I/O時保留線程–允許大量并發(fā)操作店归,而不會消耗大量線程阎抒。雖然它消除了操作系統(tǒng)線程稀缺性對吞吐量的限制,但它的代價很高:它需要所謂的異步編程風(fēng)格消痛,使用一組單獨的I/O方法且叁,這些方法不等待I/O操作完成,而是秩伞,稍后逞带,向回調(diào)表示它們的完成。如果沒有專用線程纱新,開發(fā)人員必須將其請求處理邏輯分解為小階段展氓,通常編寫為lambda表達(dá)式,然后使用API將它們組合成順序管道(請參見CompletableFuture脸爱,或所謂的“反應(yīng)性”(reactive)框架遇汞。因此,它們放棄了語言的基本順序組成運算符,如循環(huán)和try/catch塊空入。

在異步風(fēng)格中络它,請求的每個階段都可能在不同的線程上執(zhí)行,每個線程以交錯的方式運行屬于不同請求的階段歪赢。這對理解程序行為具有深刻的影響:堆棧跟蹤不提供可用的上下文化戳,調(diào)試器無法逐步完成請求處理邏輯,分析器無法將操作的成本與其調(diào)用者關(guān)聯(lián)起來轨淌。在使用Java時迂烁,編寫lambda表達(dá)式是可以管理的Stream API在短管道中處理數(shù)據(jù),但當(dāng)應(yīng)用程序中的所有請求處理代碼都必須以這種方式編寫時递鹉,就會有問題盟步。這種編程風(fēng)格與Java平臺不一致,因為應(yīng)用程序的并發(fā)單位–異步管道–不再是平臺的并發(fā)單位躏结。

使用虛擬線程保留線程每請求樣式

為了使應(yīng)用程序能夠擴(kuò)展却盘,同時與平臺保持和諧,我們應(yīng)該通過更有效地實現(xiàn)線程來努力保留每個請求的線程風(fēng)格媳拴,這樣它們就可以更豐富黄橘。操作系統(tǒng)無法更有效地實現(xiàn)操作系統(tǒng)線程,因為不同的語言和運行時以不同的方式使用線程堆棧屈溉。但是塞关,Java運行時可以以一種將Java線程與操作系統(tǒng)線程的一對一對應(yīng)關(guān)系分開的方式實現(xiàn)Java線程。就像操作系統(tǒng)通過將大量虛擬地址空間映射到有限數(shù)量的物理RAM來給人豐富內(nèi)存的錯覺一樣子巾,Java運行時也可以通過將大量虛擬線程映射到少量操作系統(tǒng)線程來給人豐富線程的錯覺帆赢。

虛擬線程是java.lang.Thread的一個實例,它不綁定到特定的操作系統(tǒng)線程线梗。相比之下椰于,平臺線程是java.lang.Thread的實例,以傳統(tǒng)方式實現(xiàn)仪搔,作為操作系統(tǒng)線程周圍的精簡包裝器瘾婿。

線程每請求樣式中的應(yīng)用程序代碼可以在虛擬線程中運行整個請求持續(xù)時間,但虛擬線程僅在CPU上執(zhí)行計算時消耗操作系統(tǒng)線程烤咧。結(jié)果是與異步風(fēng)格相同的可擴(kuò)展性偏陪,只是它是透明的:當(dāng)在虛擬線程中運行的代碼調(diào)用java中的阻塞I/O操作的java.* API時,運行時執(zhí)行非阻塞操作系統(tǒng)調(diào)用煮嫌,并自動掛起虛擬線程笛谦,直到稍后可以恢復(fù)。對于Java開發(fā)人員來說立膛,虛擬線程只是創(chuàng)建起來很便宜揪罕,而且?guī)缀鯚o限豐富的線程梯码。硬件利用率接近最佳嚷兔,允許高水平的并發(fā)性邦鲫,從而實現(xiàn)高吞吐量嘹裂,同時應(yīng)用程序與Java平臺及其工具的多線程設(shè)計保持和諧行剂。

虛擬線程的含義

虛擬線程既便宜又豐富考蕾,因此永遠(yuǎn)不應(yīng)該池化:應(yīng)該為每個應(yīng)用程序任務(wù)創(chuàng)建一個新的虛擬線程匙赞。因此冶匹,大多數(shù)虛擬線程都是短暫的撒蟀,并且具有淺調(diào)用堆棧椰弊,執(zhí)行的時間只需單個HTTP客戶端調(diào)用或單個JDBC查詢许溅。相比之下,平臺線程是重量級和昂貴的秉版,因此通常必須池化贤重。它們往往是長壽命的,具有深度的調(diào)用堆棧清焕,并在許多任務(wù)中共享并蝗。

總之,虛擬線程保留了可靠的每請求線程風(fēng)格秸妥,該風(fēng)格與Java平臺的設(shè)計和諧滚停,同時優(yōu)化地利用硬件。使用虛擬線程不需要學(xué)習(xí)新的概念粥惧,盡管它可能需要養(yǎng)成不學(xué)習(xí)的習(xí)慣键畴,以應(yīng)對當(dāng)今線程的高成本。虛擬線程不僅將幫助應(yīng)用程序開發(fā)人員突雪,還將幫助框架設(shè)計人員提供易于使用的API起惕,這些API與平臺的設(shè)計兼容,而不影響可擴(kuò)展性挂签。

描述

今天疤祭,每一個例子 java.lang.Thread在JDK中盼产,是一個平臺線程饵婆。平臺線程在底層操作系統(tǒng)線程上運行Java代碼,并在代碼的整個生命周期內(nèi)捕獲操作系統(tǒng)線程戏售。平臺線程數(shù)限制為操作系統(tǒng)線程數(shù)侨核。

虛擬線程是java.lang.Thread的一個實例,它在基礎(chǔ)操作系統(tǒng)線程上運行Java代碼灌灾,但在代碼的整個生命周期內(nèi)不捕獲操作系統(tǒng)線程搓译。這意味著許多虛擬線程可以在同一操作系統(tǒng)線程上運行其Java代碼,有效地共享它锋喜。雖然平臺線程壟斷了寶貴的操作系統(tǒng)線程些己,但虛擬線程卻不壟斷豌鸡。虛擬線程的數(shù)量可以遠(yuǎn)大于操作系統(tǒng)線程的數(shù)量。

虛擬線程是由JDK而不是操作系統(tǒng)提供的線程的輕量級實現(xiàn)段标。它們是用戶模式線程的一種形式涯冠,在其他多線程語言中已經(jīng)成功(例如,Go中的goroutine和Erlang中的進(jìn)程)逼庞。用戶態(tài)線程甚至被稱為“green threads”在Java的早期版本中蛇更,當(dāng)時操作系統(tǒng)線程還不成熟和廣泛。然而赛糟,Java的green threads都共享一個操作系統(tǒng)線程(M:1調(diào)度)派任,平臺線程被實現(xiàn)為操作系統(tǒng)線程的包裝器(1:1調(diào)度)。虛擬線程采用M:N調(diào)度璧南,其中大量(M)虛擬線程被調(diào)度在較少數(shù)量(N)的操作系統(tǒng)線程上運行掌逛。

使用虛擬線程vs平臺線程

開發(fā)者可以選擇使用虛擬線程還是平臺線程。這里是一個創(chuàng)建大量虛擬線程的示例程序司倚。程序首先獲取一個ExecutorService這將為每個提交的任務(wù)創(chuàng)建一個新的虛擬線程颤诀。然后,它提交10,000個任務(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ù)是簡單的代碼–睡眠一秒鐘–現(xiàn)代硬件可以輕松支持10000個虛擬線程同時運行此類代碼崖叫。在幕后,JDK在少量的操作系統(tǒng)線程上運行代碼拍柒,也許只有一個線程心傀。

如果此程序使用為每個任務(wù)創(chuàng)建新的平臺線程的ExecutorService,如Executor.newCachedThreadPool()拆讯,情況就會大不相同脂男。ExecutorService將嘗試創(chuàng)建10000個平臺線程,從而創(chuàng)建10000個操作系統(tǒng)線程种呐,并且該程序?qū)⒃诖蠖鄶?shù)操作系統(tǒng)上崩潰宰翅。

相反,如果程序使用從池獲取平臺線程的ExecutorService爽室,如Executor.newFixedThreadPool(200)汁讼,情況也不會好太多。ExecutorService將創(chuàng)建200個平臺線程阔墩,供所有10000個任務(wù)共享嘿架,因此許多任務(wù)將順序運行,而不是并發(fā)運行啸箫,程序?qū)⑿枰荛L時間才能完成耸彪。對于此程序,具有200個平臺線程的池只能實現(xiàn)每秒200個任務(wù)的吞吐量忘苛,而虛擬線程的吞吐量則約為每秒10000個任務(wù)(在充分預(yù)熱后)蝉娜。此外唱较,如果示例程序中的10_000更改為1_000_000,則程序?qū)⑻峤?00萬個任務(wù)召川,創(chuàng)建100萬個同時運行的虛擬線程绊汹,并(在充分預(yù)熱后)實現(xiàn)每秒約100萬個任務(wù)的吞吐量。

如果此程序中的任務(wù)執(zhí)行了一秒鐘的計算(例如扮宠,排序一個巨大的數(shù)組)西乖,而不僅僅是睡眠,那么將線程數(shù)量增加到處理器核心數(shù)量之外都沒有幫助坛增,不管它們是虛擬線程還是平臺線程获雕。虛擬線程不是更快的線程-它們運行代碼的速度并不比平臺線程快。它們的存在是為了提供規(guī)模(更高的吞吐量)收捣,而不是速度(更低的延遲)届案。它們可能比平臺線程多得多,因此根據(jù)利特爾定律罢艾,它們實現(xiàn)了更高吞吐量所需的更高并發(fā)楣颠。

換一種方式說,虛擬線程可以顯著提高應(yīng)用程序吞吐量咐蚯,當(dāng)

  • 并發(fā)任務(wù)數(shù)量較高(超過幾千個)童漩,并且
  • 工作負(fù)載不綁定CPU,因為在這種情況下春锋,線程數(shù)比處理器內(nèi)核多得多不能提高吞吐量矫膨。

虛擬線程有助于提高典型服務(wù)器應(yīng)用程序的吞吐量,正是因為此類應(yīng)用程序由大量并發(fā)任務(wù)組成期奔,這些任務(wù)花費了大量時間等待侧馅。

虛擬線程可以運行平臺線程可以運行的任何代碼。特別是呐萌,虛擬線程支持線程局部變量和線程中斷馁痴,就像平臺線程一樣。這意味著處理請求的現(xiàn)有Java代碼將很容易在虛擬線程中運行肺孤。許多服務(wù)器框架將選擇自動執(zhí)行此操作罗晕,為每個傳入請求啟動一個新的虛擬線程,并在其中運行應(yīng)用程序的業(yè)務(wù)邏輯渠旁。

以下是一個服務(wù)器應(yīng)用程序的示例攀例,它聚合了其他兩個服務(wù)的結(jié)果船逮。假設(shè)的服務(wù)器框架(未顯示)為每個請求創(chuàng)建一個新的虛擬線程顾腊,并在該虛擬線程中運行應(yīng)用程序的句柄代碼。應(yīng)用程序代碼反過來創(chuàng)建兩個新的虛擬線程挖胃,通過與第一個示例相同的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)用程序杂靶,具有簡單的阻塞代碼梆惯,可以很好地擴(kuò)展,因為它可以使用大量的虛擬線程吗垮。 Executor.newVirtualThreadPerTaskExecutor()并不是創(chuàng)建虛擬線程的唯一方法垛吗。新的java.lang.Thread.BuilderAPI,下面討論烁登,可以創(chuàng)建和啟動虛擬線程怯屉。此外,結(jié)構(gòu)化并發(fā)提供了一個更強(qiáng)大的API來創(chuàng)建和管理虛擬線程饵沧,特別是在類似于此服務(wù)器示例的代碼中锨络,通過該API,線程之間的關(guān)系將被平臺及其工具所知道狼牺。

虛擬線程是一個預(yù)覽API羡儿,默認(rèn)禁用

上面的程序使用Executors.newVirtualThreadPerTaskExecutor()方法,因此要在JDK XX上運行它們是钥,必須按以下方式啟用預(yù)覽API:

  • 使用javac --release XX --enable-preview Main.java編譯程序掠归,并使用java--enable-preview Main運行程序;或悄泥,
  • 當(dāng)使用源代碼啟動器虏冻,使用java --release XX --enable-preview Main.java;運行程序;或弹囚,
  • 使用時jshell兄旬,以jshell --enable-preview開頭

不池化虛擬線程

開發(fā)人員通常會將應(yīng)用程序代碼從基于線程池的傳統(tǒng)ExecutorService遷移到虛擬線程每任務(wù)ExecutorService。線程池和所有資源池一樣余寥,旨在共享昂貴的資源领铐,但虛擬線程并不昂貴,而且永遠(yuǎn)不需要將它們池化宋舷。

開發(fā)人員有時使用線程池來限制對有限資源的并發(fā)訪問绪撵。例如,如果一個服務(wù)不能處理超過20個并發(fā)請求祝蝠,則通過提交到大小為20的池的任務(wù)執(zhí)行對該服務(wù)的所有訪問將確保這一點音诈。由于平臺線程的高成本使線程池?zé)o處不在,這種成語也變得無處不在绎狭,但開發(fā)人員不應(yīng)該被誘惑池虛擬線程以限制并發(fā)细溅。專門為此目的設(shè)計的構(gòu)造,如信號量儡嘶,應(yīng)用于保護(hù)對有限資源的訪問喇聊。這比線程池更有效和方便,也更安全蹦狂,因為線程本地數(shù)據(jù)不會意外從一個任務(wù)泄露到另一個任務(wù)的風(fēng)險誓篱。

觀察虛擬線程

編寫清晰的代碼并不是完整的故事朋贬。清楚地表示正在運行的程序的狀態(tài)對于故障排除、維護(hù)和優(yōu)化也是必不可少的窜骄,JDK長期以來一直提供調(diào)試锦募、分析和監(jiān)控線程的機(jī)制。這些工具應(yīng)該對虛擬線程執(zhí)行同樣的操作–也許可以對其大量的操作進(jìn)行一些調(diào)節(jié)–因為它們畢竟是java.lang.Thread的實例邻遏。

Java調(diào)試器可以逐步瀏覽虛擬線程糠亩、顯示調(diào)用堆棧和檢查堆棧幀中的變量。JDK Flight Recorder (JFR)是JDK的低開銷分析和監(jiān)控機(jī)制准验,可以將應(yīng)用程序代碼中的事件(如對象分配和I/O操作)與正確的虛擬線程關(guān)聯(lián)削解。這些工具不能為以異步樣式編寫的應(yīng)用程序執(zhí)行這些操作。在這種風(fēng)格中沟娱,任務(wù)與線程無關(guān)氛驮,因此調(diào)試器無法顯示或操作任務(wù)的狀態(tài),分析器也無法判斷任務(wù)等待I/O的時間济似。

線程轉(zhuǎn)儲是另一個流行的工具矫废,用于對以線程每請求風(fēng)格編寫的應(yīng)用程序進(jìn)行故障排除。不幸的是砰蠢,JDK的傳統(tǒng)線程轉(zhuǎn)儲蓖扑,使用jstack或jcmd獲得,提供了一個線程的扁平列表台舱。這適合幾十個或數(shù)百個平臺線程律杠,但不適合數(shù)千個或數(shù)百萬個虛擬線程。因此竞惋,我們不會擴(kuò)展傳統(tǒng)的線程轉(zhuǎn)儲以包括虛擬線程柜去,而是在jcmd中引入一種新的線程轉(zhuǎn)儲,以將虛擬線程與平臺線程一起呈現(xiàn)拆宛,所有這些線程都以有意義的方式分組嗓奢。當(dāng)程序使用時,可以顯示出線程之間更豐富的關(guān)系結(jié)構(gòu)化并發(fā)浑厚。

由于可視化和分析大量線程可以從工具中受益股耽,jcmd除了純文本外,還可以以JSON格式發(fā)出新的線程轉(zhuǎn)儲:

$ jcmd <pid> Thread.dump_to_file -format=json <file>

新的線程轉(zhuǎn)儲格式列出了在網(wǎng)絡(luò)I/O操作中被阻止的虛擬線程钳幅,以及由上面所示的new-thread-per-task(一任務(wù)一線程)ExecutorService創(chuàng)建的虛擬線程物蝙。它不包括對象地址、鎖敢艰、JNI統(tǒng)計信息诬乞、堆統(tǒng)計信息和傳統(tǒng)線程轉(zhuǎn)儲中出現(xiàn)的其他信息。此外,由于它可能需要列出大量線程丽惭,因此生成新的線程轉(zhuǎn)儲不會暫停應(yīng)用程序击奶。 以下是這樣的線程轉(zhuǎn)儲的示例辈双,取自與上面第二個示例類似的應(yīng)用程序责掏,在JSON查看器中呈現(xiàn)(見下圖):

[圖片上傳失敗...(image-3e375a-1695296921208)]

由于虛擬線程是在JDK中實現(xiàn)的,并且不綁定到任何特定的操作系統(tǒng)線程湃望,因此它們對操作系統(tǒng)是不可見的换衬,而操作系統(tǒng)不知道它們的存在。操作系統(tǒng)級監(jiān)控將觀察到JDK進(jìn)程使用的操作系統(tǒng)線程比虛擬線程少证芭。

調(diào)度虛擬線程

要做有用的工作瞳浦,需要調(diào)度線程,即分配在處理器核心上執(zhí)行废士。對于作為操作系統(tǒng)線程實現(xiàn)的平臺線程叫潦,JDK依賴于操作系統(tǒng)中的調(diào)度程序。相比之下官硝,對于虛擬線程矗蕊,JDK有自己的調(diào)度程序。JDK的調(diào)度程序?qū)⑻摂M線程分配給平臺線程氢架,而不是直接將虛擬線程分配給處理器(這就是前面提到的虛擬線程的M:N調(diào)度)傻咖。然后,操作系統(tǒng)將像往常一樣調(diào)度平臺線程岖研。

JDK的虛擬線程調(diào)度程序是一個竊取工作的工具ForkJoinPool在FIFO模式下工作卿操。調(diào)度程序的并行性是可用于調(diào)度虛擬線程的平臺線程數(shù)。默認(rèn)情況下孙援,它等于可用處理器害淤,但它可以使用系統(tǒng)屬性jdk.virtualThreadScheduler.parallelism進(jìn)行調(diào)整。請注意拓售,此ForkJoinPool不同于公共池筝家,例如,它用于并行流的實現(xiàn)邻辉,并在后進(jìn)先出模式下工作溪王。

調(diào)度程序分配虛擬線程的平臺線程稱為虛擬線程的載體。虛擬線程可以在其生命周期內(nèi)在不同的載體上調(diào)度值骇;換句話說莹菱,調(diào)度程序不保持虛擬線程和任何特定平臺線程之間的親和性。但是吱瘩,從Java代碼的角度來看道伟,正在運行的虛擬線程在邏輯上獨立于其當(dāng)前載體:

  • 虛擬線程無法使用運營商的身份。Thread.currentThread()返回的值始終是虛擬線程本身。
  • 載體和虛擬線程的堆棧跟蹤是分開的蜜徽。虛擬線程中拋出的異常將不包括載體的堆棧幀祝懂。線程轉(zhuǎn)儲不會在虛擬線程的堆棧中顯示載體的堆棧幀,反之亦然拘鞋。
  • 載體的線程局部變量對虛擬線程不可用砚蓬,反之亦然。

此外盆色,從Java代碼的角度來看灰蛙,虛擬線程及其載體暫時共享操作系統(tǒng)線程的事實是不可見的。相比之下隔躲,從本機(jī)代碼的角度來看摩梧,虛擬線程和它的載體都運行在同一個本機(jī)線程上。因此宣旱,在同一虛擬線程上多次調(diào)用的本機(jī)代碼可能會在每次調(diào)用時觀察到不同的操作系統(tǒng)線程標(biāo)識符仅父。

調(diào)度程序當(dāng)前不為虛擬線程實現(xiàn)分時。分時是對占用分配數(shù)量的CPU時間的線程的強(qiáng)制搶占浑吟。雖然分時使用幾百個平臺線程可以有效笙纤,但目前還不清楚分時使用一百萬個虛擬線程是否會那么有效。

執(zhí)行虛擬線程

要利用虛擬線程买置,沒有必要重寫程序粪糙。虛擬線程不要求或期望應(yīng)用程序代碼顯式地將控制權(quán)交回調(diào)度程序;換句話說忿项,虛擬線程是不合作的蓉冈。用戶代碼不得假設(shè)虛擬線程如何或何時分配給平臺線程,就如假設(shè)平臺線程如何或何時分配給處理器內(nèi)核一樣轩触。

要在虛擬線程中運行代碼寞酿,JDK的虛擬線程調(diào)度程序通過將虛擬線程掛載在平臺線程上,分配虛擬線程在平臺線程上執(zhí)行脱柱。這使得平臺線程成為虛擬線程的載體伐弹。稍后,在運行一些代碼后榨为,虛擬線程可以從其載體上卸載惨好。在這一點上,平臺線程是空閑的随闺,因此調(diào)度程序可以在其上裝載不同的虛擬線程日川,從而使其再次成為載體。

通常矩乐,當(dāng)虛擬線程阻塞I/O或JDK中的其他阻塞操作時龄句,虛擬線程將卸載回论,如BlockingQueue.take()。當(dāng)阻塞操作準(zhǔn)備完成時(例如分歇,已在套接字上接收字節(jié))傀蓉,它將虛擬線程提交回調(diào)度程序,調(diào)度程序?qū)⒃谳d體上裝載虛擬線程以恢復(fù)執(zhí)行职抡。

虛擬線程的裝載和卸載頻繁且透明地進(jìn)行葬燎,并且不會阻止任何操作系統(tǒng)線程。例如繁调,前面顯示的服務(wù)器應(yīng)用程序包括以下代碼行萨蚕,其中包含對阻止操作的調(diào)用:

response.send(future1.get() + future2.get());

這些操作將導(dǎo)致虛擬線程多次裝載和卸載靶草,通常每次調(diào)用get()一次,在send(…)中執(zhí)行I/O過程中可能多次。

JDK中的絕大多數(shù)阻塞操作將卸載虛擬線程已旧,釋放其載體和底層操作系統(tǒng)線程來承擔(dān)新的工作先改。但是,JDK中的一些阻塞操作不會卸載虛擬線程派继,因此會阻塞其載體和底層操作系統(tǒng)線程宾袜。這是因為操作系統(tǒng)級別(例如,許多文件系統(tǒng)操作)或JDK級別(例如驾窟,Object.wait())庆猫。這些阻塞操作的實現(xiàn)將通過臨時擴(kuò)展調(diào)度程序的并行性來補償操作系統(tǒng)線程的捕獲。因此绅络,調(diào)度程序的ForkJoinPool中的平臺線程數(shù)量可能暫時超過可用處理器的數(shù)量月培。調(diào)度程序可用的最大平臺線程數(shù)可以使用系統(tǒng)屬性jdk.virtualThreadScheduler.maxPoolSize進(jìn)行調(diào)整。

在兩種情況下恩急,虛擬線程在阻塞操作期間無法卸載杉畜,因為它被固定在其載體上:

  • 當(dāng)它在同步塊或方法中執(zhí)行代碼時,或
  • 當(dāng)它執(zhí)行本機(jī)方法或外來函數(shù).

固定不會使應(yīng)用程序不正確衷恭,但可能會妨礙其可擴(kuò)展性此叠。如果虛擬線程在被固定時執(zhí)行阻塞操作,如I/O或BlockingQueue.take()随珠,則在操作期間灭袁,其載體和基礎(chǔ)操作系統(tǒng)線程將被阻塞。長時間頻繁固定可能會通過捕獲運營商而損害應(yīng)用程序的可擴(kuò)展性窗看。

調(diào)度程序不會通過擴(kuò)展其并行度來補償固定茸歧。相反,通過修改頻繁運行的同步塊或方法烤芦,并保護(hù)要使用的潛在長I/O操作举娩,避免頻繁和長期的固定java.util.concurrent.locks.ReentrantLock相反,不需要替換不經(jīng)常使用的同步塊和方法(例如,僅在啟動時執(zhí)行)或保護(hù)內(nèi)存操作铜涉。和往常一樣智玻,努力保持鎖定策略簡單明了。

新的診斷有助于將代碼遷移到虛擬線程芙代,并評估是否應(yīng)將同步的特定使用替換為java.util.concurrent鎖:

  • 當(dāng)線程在被固定時阻塞時吊奢,將發(fā)出JDK Flight Recorder (JFR) 事件.
  • 系統(tǒng)屬性jdk.tracePinnedThreads在線程被固定時阻塞時觸發(fā)堆棧跟蹤。使用-Djdk.tracePinnedThreads=full運行時纹烹,當(dāng)線程在固定時阻塞時页滚,打印完整的堆棧跟蹤,本機(jī)幀和持有監(jiān)視器的幀高亮顯示铺呵。使用-Djdk.tracePinnedThreads=short將輸出限制為僅有問題的幀裹驰。

我們也許能夠在未來的版本中刪除上面的第一個限制。第二個限制是與本機(jī)代碼正確交互所必需的片挂。

內(nèi)存使用和與垃圾回收的交互

虛擬線程的堆棧作為堆棧塊對象存儲在Java的垃圾收集堆中幻林。堆棧隨著應(yīng)用程序運行而增長和收縮,既是為了提高內(nèi)存效率音念,又是為了容納任意深度的堆棧(最高可達(dá)JVM配置的平臺線程堆棧大谢取)。這種效率使大量虛擬線程得以實現(xiàn)闷愤,從而使服務(wù)器應(yīng)用程序中線程每請求風(fēng)格的持續(xù)生存能力得以實現(xiàn)整葡。

在上面的第二個示例中,請記住讥脐,假設(shè)框架通過創(chuàng)建新的虛擬線程并調(diào)用句柄方法來處理每個請求遭居;即使它在深度調(diào)用堆棧的末尾調(diào)用句柄(在身份驗證、事務(wù)等之后)攘烛,句柄本身會生成多個僅執(zhí)行短期任務(wù)的虛擬線程魏滚。因此,對于每個具有深調(diào)用棧的虛擬線程坟漱,將有多個具有淺調(diào)用棧的虛擬線程消耗很少的內(nèi)存鼠次。

一般來說,虛擬線程所需的堆空間和垃圾收集器活動量很難與異步代碼相比芋齿。處理請求的應(yīng)用程序代碼通常必須跨I/O操作維護(hù)數(shù)據(jù)腥寇;線程每請求代碼可以將數(shù)據(jù)保留在局部變量中,局部變量存儲在堆中的虛擬線程堆棧上觅捆。而異步代碼必須在堆對象中保留相同數(shù)據(jù)赦役,這些數(shù)據(jù)從管道的一個階段傳遞到下一個階段。一方面栅炒,虛擬線程所需的堆棧幀布局比緊湊對象更浪費掂摔;另一方面术羔,虛擬線程在許多情況下(取決于低級GC交互)可以變異和重用其堆棧,而異步管道總是需要分配新對象乙漓〖独總體而言,線程每請求與異步代碼的堆消耗和垃圾收集器活動應(yīng)該大致相似叭披。隨著時間的推移寥殖,我們希望使虛擬線程堆棧的內(nèi)部表示更加緊湊。

與平臺線程棧不同涩蜘,虛擬線程棧不是GC根嚼贡,因此其中包含的引用不會被執(zhí)行并發(fā)掃描的垃圾收集器在停止世界暫停中遍歷。這也意味著同诫,如果虛擬線程在上被阻塞粤策,例如 BlockingQueue.take(),并且沒有其他線程可以獲得對虛擬線程或隊列的引用剩辟,那么該線程可以被垃圾收集-這很好掐场,因為虛擬線程永遠(yuǎn)不會被中斷或解除阻塞往扔。當(dāng)然贩猎,如果虛擬線程正在運行,或者如果它被阻止萍膛,并且可能會被取消阻止吭服,則它不會被垃圾收集。

虛擬線程的當(dāng)前限制是G1 GC不支持巨大的堆棧塊對象蝗罗。如果虛擬線程的堆棧達(dá)到區(qū)域大小的一半(通常為512KB)艇棕,則可能會引發(fā)StackOverflowError。

詳細(xì)更改

其余小節(jié)詳細(xì)介紹了在Java平臺及其實現(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)建虛擬線程和平臺線程的新API沼琉。例如,Thread thread = Thread.ofVirtual().name("duke").unstarted(runnable);創(chuàng)建一個名為“duke”的新未啟動虛擬線程桩匪。
  • Thread.startVirtualThread(Runnable)是創(chuàng)建和啟動虛擬線程的方便方法打瘪。
  • Thread.Builder可以創(chuàng)建一個線程,也可以創(chuàng)建一個ThreadFactory傻昙,然后可以創(chuàng)建具有相同屬性的多個線程闺骚。
  • Thread.isVirtual()測試線程是否為虛擬線程。
  • Thread.join 和 Thread.sleep接受等待和睡眠時間作為java.time.Duration.
  • 新的最終方法Thread.threadId() 返回線程的標(biāo)識符∽钡担現(xiàn)有非最終方法 Thread.getId() 現(xiàn)在已棄用僻爽。
  • Thread.getAllStackTraces() 現(xiàn)在返回所有平臺線程而不是所有線程的映射。

java.lang.Thread API在其他方面保持不變贾惦。線程類定義的構(gòu)造函數(shù)創(chuàng)建平臺線程胸梆,與前面一樣敦捧。沒有新的公共構(gòu)造函數(shù)。

虛擬線程和平臺線程之間的主要API區(qū)別是:

  • 公共線程構(gòu)造函數(shù)無法創(chuàng)建虛擬線程碰镜。
  • 虛擬線程始終是守護(hù)線程绞惦。Thread.setDaemon(boolean) 方法無法將虛擬線程更改為非守護(hù)線程。
  • 虛擬線程的固定優(yōu)先級為 Thread.NORM_PRIORITY洋措。 Thread.setPriority(int) 方法對虛擬線程沒有影響济蝉。此限制可能會在未來的版本中重新討論。
  • 虛擬線程不是線程組的活動成員菠发。在虛擬線程上調(diào)用時王滤,Thread.getThreadGroup() 返回名為“VirtualThreads”的占位線程組。Thread.Builder API沒有定義設(shè)置虛擬線程線程的線程組的方法滓鸠。
  • 使用SecurityManager集運行時雁乡,虛擬線程沒有權(quán)限。
  • 虛擬線程不支持 stop()糜俗、suspend()踱稍、 resume()方法。這些方法在虛擬線程上調(diào)用時引發(fā)異常悠抹。

線程局部變量

虛擬線程支持線程局部變量(ThreadLocal)和可繼承的線程局部變量(InheritableThreadLocal)珠月,就像平臺線程一樣,因此它們可以運行使用線程本地程序的現(xiàn)有代碼楔敌。但是啤挎,由于虛擬線程可能非常多,請在仔細(xì)考慮后使用線程本地卵凑。特別是庆聘,不要使用線程本地在線程池中共享同一線程的多個任務(wù)之間匯集昂貴的資源。虛擬線程不應(yīng)池化勺卢,因為每個線程在其生命周期內(nèi)只運行一個任務(wù)伙判。我們已經(jīng)從java.base模塊中刪除了線程本地的許多使用,以準(zhǔn)備虛擬線程黑忱,以減少在使用數(shù)百萬線程運行時的內(nèi)存占用宴抚。

此外:

  • Thread.Builder API定義了創(chuàng)建線程時選擇退出線程本地的方法。它還定義了選擇退出繼承可繼承線程局部的初始值的方法杨何。當(dāng)從不支持線程本地的線程調(diào)用時酱塔,ThreadLocal.get() 返回初始值和線程本地集(T)拋出異常。
  • 遺產(chǎn)上下文類加載器現(xiàn)在指定為像本地可繼承線程一樣工作危虱。如果Thread.setContextClassLoader(ClassLoader)在不支持線程本地的線程上調(diào)用羊娃,然后引發(fā)異常。

作用域-局部變量對于某些用例埃跷,可能會被證明是線程本地的更好替代方案蕊玷。

java.util.concurrent

支持鎖定的原始API邮利。java.util.concurrent.LockSupport,現(xiàn)在支持虛擬線程:駐留虛擬線程釋放基礎(chǔ)載體線程以執(zhí)行其他工作垃帅,取消駐留虛擬線程計劃它繼續(xù)延届。對LockSupport的此更改使所有使用它的API(鎖、信號量贸诚、阻塞隊列等)在虛擬線程中調(diào)用時都能優(yōu)雅地停放方庭。

此外:

  • Executors.newThreadPerTaskExecutor(ThreadFactory) 和 Executors.newVirtualThreadPerTaskExecutor() 創(chuàng)建一個ExecutorService,為每個任務(wù)創(chuàng)建一個新線程酱固。這些方法支持遷移和與使用線程池和ExecutorService的現(xiàn)有代碼的互操作性械念。
  • 執(zhí)行器服務(wù)現(xiàn)在延伸可自動關(guān)閉,因此允許此API與上面示例中所示的try-with-resource構(gòu)造一起使用运悲。
  • Future現(xiàn)在定義了獲取已完成任務(wù)的結(jié)果或異常以及獲取任務(wù)狀態(tài)的方法龄减。結(jié)合起來,這些添加使我們可以很容易地將Future對象用作流的元素班眯,過濾Future流以查找已完成的任務(wù)希停,然后映射以獲得結(jié)果流。這些方法也將有助于為結(jié)構(gòu)化并發(fā)署隘。

網(wǎng)絡(luò)

java.net和java.nio.channels包中的網(wǎng)絡(luò)API的實現(xiàn)現(xiàn)在與虛擬線程一起工作:對虛擬線程的操作宠能,該操作阻止建立網(wǎng)絡(luò)連接或從套接字讀取,釋放基礎(chǔ)載體線程以執(zhí)行其他工作定踱。

為了允許中斷和取消棍潘,阻塞I/O方法定義為java.net.Socket、ServerSocket和DatagramSocket現(xiàn)在指定為可中斷的在虛擬線程中調(diào)用時:中斷套接字上阻塞的虛擬線程將取消鎖定線程并關(guān)閉套接字崖媚。阻止這些類型套接字上的I/O操作,當(dāng)從 InterruptibleChannel一直是可中斷的恤浪,因此此更改將這些API在創(chuàng)建時的行為與從通道獲取時的行為對齊畅哑。

java.io

java.io包提供字節(jié)流和字符流的API。這些API的實現(xiàn)是高度同步的水由,需要進(jìn)行更改荠呐,以避免在虛擬線程中使用它們時固定。

作為背景砂客,面向字節(jié)的輸入/輸出流沒有被指定為線程安全泥张,也沒有指定當(dāng)線程在讀或?qū)懛椒ㄖ斜蛔枞麜r調(diào)用close()時的預(yù)期行為。在大多數(shù)情況下鞠值,使用來自多個并發(fā)線程的特定輸入或輸出流是沒有意義的媚创。面向字符的讀取器/寫入器也沒有被指定為線程安全的,但它們確實公開了子類的鎖對象彤恶。除了固定之外钞钙,這些類中的同步是有問題的和不一致的鳄橘;例如,InputStreamReader和OutputStreamWriter使用的流解碼器和編碼器在流對象上同步芒炼,而不是在鎖對象上同步瘫怜。

為防止固定,實現(xiàn)現(xiàn)在的工作方式如下:

  • BufferedInputStream本刽、BufferedOutputStream鲸湃、BufferedReader、BufferedWriter子寓、PrintStream和PrintWriter現(xiàn)在在直接使用時使用顯式鎖而不是監(jiān)視器唤锉。這些類在子類時與以前一樣同步。
  • InputStreamReader和OutputStreamWriter使用的流解碼器和編碼器現(xiàn)在使用與封閉的InputStreamReader或OutputStreamWriter相同的鎖别瞭。

更進(jìn)一步窿祥,消除所有這些通常不必要的鎖定超出了本JEP的范圍。

此外蝙寨,BufferedOutputStream晒衩、BufferedWriter和OutputStreamWriter的流編碼器使用的緩沖區(qū)的初始大小現(xiàn)在更小,以便在堆中有許多流或?qū)懭氤绦驎r減少內(nèi)存使用-如果有是一百萬個虛擬線程墙歪,每個線程在套接字連接上都有一個緩沖流听系。

Java Native Interface (JNI)

JNI定義了一個新函數(shù)IsVirtualThread,用于測試對象是否為虛擬線程虹菲。

JNI規(guī)范在其他方面保持不變靠胜。

調(diào)試

調(diào)試體系結(jié)構(gòu)由三個接口組成:JVM Tool Interface (JVM TI)、Java Debug Wire Protocol (JDWP)和Java Debug Interface (JDI)”显矗現(xiàn)在浪漠,所有三個接口都支持虛擬線程。

更新到JVM TI是:

  • 大多數(shù)用jthread 調(diào)用的函數(shù)(即霎褐,對線程對象的JNI引用)都可以用對虛擬線程的引用調(diào)用址愿。虛擬線程不支持少量函數(shù),即PopFrame冻璃、ForceEarlyReturn响谓、StopThread、AgentStartFunction和GetThreadCpuTime省艳。SetLocal*函數(shù)僅限于在斷點或單步事件掛起的虛擬線程的最頂層幀中設(shè)置局部變量娘纷。
  • 現(xiàn)在,GetAllThreads和GetAllStackTraces函數(shù)被指定為返回所有平臺線程跋炕,而不是所有線程赖晶。
  • 除在早期VM啟動或堆迭代期間發(fā)布的事件外,所有事件都可以在虛擬線程的上下文中調(diào)用事件回調(diào)枣购。
  • 掛起/恢復(fù)實現(xiàn)允許調(diào)試器掛起和恢復(fù)虛擬線程嬉探,并允許在掛起虛擬線程時掛起載體線程擦耀。
  • 一項新功能can_support_virtual_threads,使代理可以更精細(xì)地控制虛擬線程的線程開始和結(jié)束事件涩堤。
  • 新函數(shù)支持虛擬線程的批量掛起和恢復(fù)眷蜓;這些函數(shù)需要can_support_virtual_threads功能。

現(xiàn)有的JVM TI代理將與以前一樣工作胎围,但如果它們調(diào)用虛擬線程不支持的函數(shù)吁系,則可能會遇到錯誤。當(dāng)不知道虛擬線程的代理與使用虛擬線程的應(yīng)用程序一起使用時白魂,就會出現(xiàn)這些情況汽纤。對某些代理來說,更改GetAllThreads以返回僅包含平臺線程的數(shù)組可能是一個問題福荸。啟用ThreadStart和ThreadEnd事件的現(xiàn)有代理可能會遇到性能問題蕴坪,因為它們無法將這些事件限制在平臺線程上。

更新到JDWP是:

  • 一個新命令允許調(diào)試器測試線程是否為虛擬線程敬锐。
  • EventRequest命令上的新修飾符允許調(diào)試器將線程開始和結(jié)束事件限制在平臺線程上背传。

更新到JDI是:

  • 一種新的方法com.sun.jdi.ThreadReference測試線程是否為虛擬線程。
  • 中的新方法com.sun.jdi.request.ThreadStartRequest和com.sun.jdi.request.ThreadDeathRequest將為請求生成的事件限制為平臺線程台夺。

如上所述径玖,虛擬線程不被視為線程組中的活動線程。因此颤介,JVM TI函數(shù)GetThreadGroupCalt梳星、JDWP命令ThreadGroupReference/Children和JDI方法 com.sun.jdi.ThreadGroupReference.threads() 返回的線程列表僅包括平臺線程。

JDK Flight Recorder (JFR)

JFR支持具有多個新事件的虛擬線程:

  • jdk.VirtualThreadStart和jdk.VirtualThreadEnd表示虛擬線程的開始和結(jié)束滚朵。默認(rèn)情況下冤灾,這些事件是禁用的。
  • jdk.VirtualThreadPinned表示虛擬線程在被固定時被駐留始绍,即瞳购,沒有釋放其載體線程(參見限制)。此事件默認(rèn)啟用亏推,閾值為20ms。
  • jdk.VirtualThreadSubmitFailed表示啟動或取消駐留虛擬線程失敗年堆,可能是由于資源問題吞杭。默認(rèn)情況下,此事件處于啟用狀態(tài)变丧。

Java Management Extensions (JMX)

一種新的方法com.sun.management.HotSpotDiagnosticsMXBean 生成描述的新式線程轉(zhuǎn)儲以上芽狗。該方法也可以通過平臺間接調(diào)用 MBeanServer從本地或遠(yuǎn)程JMX工具。

java.lang.management.ThreadMXBean 僅支持平臺線程的監(jiān)控和管理痒蓬。

java.lang.ThreadGroup

java.lang.ThreadGroup是一個用于分組線程的舊API童擎,在現(xiàn)代應(yīng)用程序中很少使用滴劲,不適合分組虛擬線程。我們現(xiàn)在不推薦并降級它顾复,并希望在未來引入一個新的線程組織結(jié)構(gòu)班挖,作為結(jié)構(gòu)化并發(fā)。

作為背景芯砸,ThreadGroup API可以追溯到Java 1.0萧芙。它最初旨在提供作業(yè)控制操作,如停止組中的所有線程〖偕ィ現(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和實現(xiàn)還存在許多重大問題:

  • 銷毀線程組的API和機(jī)制存在缺陷。
  • API要求實現(xiàn)引用組中的所有活動線程几莽。這將為線程創(chuàng)建迅办、線程啟動和線程終止增加同步和競爭開銷。
  • API定義了 enumerate() 方法章蚣,這些方法本身是有效的站欺。
  • API定義了suspend()、resume()和stop()方法纤垂,這些方法本身就容易死鎖和不安全矾策。

現(xiàn)在指定、不建議使用和降級ThreadGroup 峭沦,如下所示:

  • 刪除了顯式銷毀線程組的能力:最終不建議使用的destroy()方法什么都不做贾虽。
  • 守護(hù)程序線程組的概念已刪除:守護(hù)程序狀態(tài)設(shè)置并由最終棄用的setDaemon(boolean) 和 isDaemon() 方法被忽略。
  • 該實現(xiàn)不再保留對子組的強(qiáng)引用吼鱼。當(dāng)線程組中沒有活動線程蓬豁,并且沒有其他任何其他線程保持線程組活動時,線程組現(xiàn)在有資格被垃圾收集菇肃。
  • 最終不建議使用的suspend()地粪、resume()和stop()方法總是拋出異常。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末琐谤,一起剝皮案震驚了整個濱河市蟆技,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖质礼,帶你破解...
    沈念sama閱讀 217,406評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件旺聚,死亡現(xiàn)場離奇詭異,居然都是意外死亡眶蕉,警方通過查閱死者的電腦和手機(jī)砰粹,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,732評論 3 393
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來妻坝,“玉大人伸眶,你說我怎么就攤上這事」粝埽” “怎么了厘贼?”我有些...
    開封第一講書人閱讀 163,711評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長圣拄。 經(jīng)常有香客問我嘴秸,道長,這世上最難降的妖魔是什么庇谆? 我笑而不...
    開封第一講書人閱讀 58,380評論 1 293
  • 正文 為了忘掉前任岳掐,我火速辦了婚禮,結(jié)果婚禮上饭耳,老公的妹妹穿的比我還像新娘串述。我一直安慰自己,他們只是感情好寞肖,可當(dāng)我...
    茶點故事閱讀 67,432評論 6 392
  • 文/花漫 我一把揭開白布兆沙。 她就那樣靜靜地躺著嵌屎,像睡著了一般抡草。 火紅的嫁衣襯著肌膚如雪怔揩。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,301評論 1 301
  • 那天琼稻,我揣著相機(jī)與錄音吮螺,去河邊找鬼。 笑死帕翻,一個胖子當(dāng)著我的面吹牛鸠补,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播嘀掸,決...
    沈念sama閱讀 40,145評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼莫鸭,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了横殴?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,008評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎衫仑,沒想到半個月后梨与,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,443評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡文狱,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,649評論 3 334
  • 正文 我和宋清朗相戀三年粥鞋,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片瞄崇。...
    茶點故事閱讀 39,795評論 1 347
  • 序言:一個原本活蹦亂跳的男人離奇死亡呻粹,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出苏研,到底是詐尸還是另有隱情等浊,我是刑警寧澤,帶...
    沈念sama閱讀 35,501評論 5 345
  • 正文 年R本政府宣布摹蘑,位于F島的核電站筹燕,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏衅鹿。R本人自食惡果不足惜撒踪,卻給世界環(huán)境...
    茶點故事閱讀 41,119評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望大渤。 院中可真熱鬧制妄,春花似錦、人聲如沸泵三。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,731評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽切黔。三九已至砸脊,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間纬霞,已是汗流浹背凌埂。 一陣腳步聲響...
    開封第一講書人閱讀 32,865評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留诗芜,地道東北人瞳抓。 一個月前我還...
    沈念sama閱讀 47,899評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像伏恐,于是被迫代替她去往敵國和親孩哑。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,724評論 2 354

推薦閱讀更多精彩內(nèi)容