OpenJDK织机和结构化并发
Project Loom是Hotspot Group贊助的項(xiàng)目之一,旨在向JAVA世界提供高吞吐量和輕量級(jí)的并發(fā)模型。 在撰寫本文時(shí),Loom項(xiàng)目仍處于積極開發(fā)中,其API可能會(huì)更改。
為什么要織機(jī)?
每個(gè)新項(xiàng)目可能會(huì)出現(xiàn)的第一個(gè)問題是為什么?
為什么我們需要學(xué)習(xí)新的東西,它對(duì)我們有幫助? (如果確實(shí)如此)
因此,要專門針對(duì)Loom回答這個(gè)問題,我們首先需要了解JAVA中現(xiàn)有線程系統(tǒng)如何工作的基礎(chǔ)知識(shí)。
JVM內(nèi)部產(chǎn)生的每個(gè)線程在OS內(nèi)核空間中都有一個(gè)一對(duì)一的對(duì)應(yīng)線程,并具有自己的堆棧,寄存器,程序計(jì)數(shù)器和狀態(tài)。 每個(gè)線程的最大部分可能是堆棧,堆棧大小以兆字節(jié)為單位,通常在1MB到2MB之間。
因此,這些類型的線程在啟動(dòng)和運(yùn)行時(shí)方面都很昂貴。 不可能在一臺(tái)機(jī)器上產(chǎn)生1萬(wàn)個(gè)線程并期望它能正常工作。
有人可能會(huì)問為什么我們甚至需要那么多線程? 鑒于CPU只有幾個(gè)超線程。 例如,CPU Internal Core i9總共有16個(gè)線程。
嗯,CPU并不是您的應(yīng)用程序使用的唯一資源,任何沒有I / O的軟件都只會(huì)導(dǎo)致全球變暖!
一旦線程需要I / O,OS就會(huì)嘗試為其分配所需的資源,并同時(shí)調(diào)度另一個(gè)需要CPU的線程。 因此,我們?cè)趹?yīng)用程序中擁有的線程越多,我們就越可以并行利用這些資源。
一個(gè)非常典型的示例是Web服務(wù)器。 每臺(tái)服務(wù)器都能在每個(gè)時(shí)間點(diǎn)處理數(shù)千個(gè)打開的連接,但是同時(shí)處理那么多連接要么需要數(shù)千個(gè)線程,要么需要異步非阻塞代碼( 我可能會(huì)在接下來的幾周內(nèi)撰寫另一篇文章,以解釋更多有關(guān)異步代碼 ),就像前面提到的,成千上萬(wàn)個(gè)OS線程既不是您也不是OS會(huì)滿意的!
織機(jī)如何提供幫助?
作為Project Loom的一部分,引入了一種稱為Fiber的新型線程。 光纖也稱為虛擬線程 , 綠色線程或用戶線程,因?yàn)檫@些名稱暗示完全由VM處理,并且OS甚至都不知道此類線程存在。 這意味著并非每個(gè)VM線程都需要在OS級(jí)別具有相應(yīng)的線程! 虛擬線程可能被I / O阻塞,或者等待從另一個(gè)線程獲取信號(hào),但是,與此同時(shí),其他虛擬線程也可以利用基礎(chǔ)線程!
上圖說明了虛擬線程和OS線程之間的關(guān)系。 虛擬線程可以簡(jiǎn)單地被I / O阻塞,在這種情況下,基礎(chǔ)線程將被另一個(gè)虛擬線程使用。
這些虛擬線程的內(nèi)存占用量將以千字節(jié)為單位,而不是兆字節(jié)。 如果需要,可以在生成它們之后擴(kuò)展它們的堆棧,這樣JVM不需要為它們分配大量?jī)?nèi)存。
因此,既然我們已經(jīng)有了一種非常輕巧的方式來實(shí)現(xiàn)并發(fā),我們就可以重新考慮存在于Java經(jīng)典線程中的最佳實(shí)踐。
如今,用于在Java中實(shí)現(xiàn)并發(fā)的最常用的構(gòu)造是ExecutorService的不同實(shí)現(xiàn)。 它們具有非常方便的API,并且相對(duì)易于使用。 執(zhí)行程序服務(wù)具有一個(gè)內(nèi)部線程池,用于根據(jù)開發(fā)人員定義的特征來控制可以產(chǎn)生多少個(gè)線程。 該線程池主要用于限制應(yīng)用程序創(chuàng)建的OS線程的數(shù)量,因?yàn)槿缟纤?#xff0c;它們是昂貴的資源,我們應(yīng)該盡可能地重用它們。 但是現(xiàn)在可以生成輕量級(jí)虛擬線程了,我們也可以重新考慮使用ExecutorServices的方式。
結(jié)構(gòu)化并發(fā)
結(jié)構(gòu)化并發(fā)是一種編程范式,是一種編寫易于讀取和維護(hù)的并發(fā)程序的結(jié)構(gòu)化方法。 如果代碼對(duì)并發(fā)任務(wù)有明確的入口和出口點(diǎn),則其主要思想與結(jié)構(gòu)化編程非常相似,與啟動(dòng)可能比當(dāng)前作用域持續(xù)時(shí)間更長(zhǎng)的并發(fā)任務(wù)相比,對(duì)代碼的推理要容易得多!
為了更清楚地了解結(jié)構(gòu)化并發(fā)代碼的外觀,請(qǐng)考慮以下偽代碼:
void notifyUser(User user) { try (var scope = new ConcurrencyScope()) { scope.submit( () -> notifyByEmail(user)); scope.submit( () -> notifyBySMS(user)); } LOGGER.info( "User has been notified successfully" ); }notifyUser方法應(yīng)該通過電子郵件和SMS通知用戶,并且一旦成功完成此方法將記錄一條消息。 使用結(jié)構(gòu)化并發(fā),可以保證在兩種通知方法完成后立即寫入日志。 換句話說,如果嘗試范圍在其中所有已啟動(dòng)的并發(fā)作業(yè)都完成了,那么它將完成!
注意:為了使示例簡(jiǎn)單,我們假設(shè)notifyByEmail和notifyBySMS在上面的示例中,在內(nèi)部確實(shí)處理所有可能的極端情況,并始終使其通過。
JAVA的結(jié)構(gòu)化并發(fā)
在本節(jié)中,我將通過一個(gè)非常簡(jiǎn)單的示例展示如何用JAVA編寫結(jié)構(gòu)化并發(fā)應(yīng)用程序以及Fibers如何幫助擴(kuò)展應(yīng)用程序。
我們要解決的問題
想象一下,所有I / O綁定有1萬(wàn)個(gè)任務(wù),而每個(gè)任務(wù)恰好需要100毫秒才能完成。 我們被要求編寫高效的代碼來完成這些工作。
我們使用下面定義的Job類來模仿我們的工作。
public class Job { public void doIt() { try { Thread.sleep(100l); } catch (InterruptedException e) { e.printStackTrace(); } } }第一次嘗試
在第一次嘗試中,我們使用緩存線程池和OS線程來編寫它。
public class ThreadBasedJobRunner implements JobRunner { @Override public long run(List<Job> jobs) { var start = System.nanoTime(); var executor = Executors.newCachedThreadPool(); for (Job job : jobs) { executor.submit(job::doIt); } executor.shutdown(); try { executor.awaitTermination( 1 , TimeUnit.DAYS); } catch (InterruptedException e) { e.printStackTrace(); Thread.currentThread().interrupt(); } var end = System.nanoTime(); long timeSpentInMS = Util.nanoToMS(end - start); ?return timeSpentInMS; } }在此嘗試中,我們沒有應(yīng)用Loom項(xiàng)目中的任何內(nèi)容。 只是一個(gè)緩存的線程池,以確保將使用空閑線程,而不是創(chuàng)建新線程。
讓我們看看使用此實(shí)現(xiàn)可以運(yùn)行10,000個(gè)作業(yè)所需的時(shí)間。 我使用下面的代碼來查找運(yùn)行速度最快的10個(gè)代碼。 為簡(jiǎn)單起見,未使用任何微基準(zhǔn)測(cè)試工具。
public class ThreadSleep { public static void main(String[] args) throws InterruptedException { List<Long> timeSpents = new ArrayList<>( 100 ); var jobs = IntStream.range( 0 , 10000 ).mapToObj(n -> new Job()).collect(toList()); for ( int c = 0 ; c <= 100 ; c++) { var jobRunner = new var jobRunner = ThreadBasedJobRunner(); var timeSpent = jobRunner.run(jobs); timeSpents.add(timeSpent); } Collections.sort(timeSpents); System.out.println( "Top 10 executions took:" ); timeSpents.stream().limit( 10 ) .forEach(timeSpent -> System.out.println( "%s ms" .formatted(timeSpent)) ); } }我的機(jī)器上的結(jié)果是:
執(zhí)行的前10名:
694毫秒
695毫秒 696毫秒 696毫秒 696毫秒 697毫秒 699毫秒 700毫秒 700毫秒 700毫秒
到目前為止,我們有一個(gè)代碼,最好情況下大約需要700毫秒才能在我的計(jì)算機(jī)上運(yùn)行10,000個(gè)作業(yè)。 讓我們這次使用Loom功能實(shí)現(xiàn)JobRunner。
第二次嘗試(使用光纖)
在使用Fibers或Virtual Threads的實(shí)現(xiàn)中,我還將以結(jié)構(gòu)化的方式對(duì)并發(fā)進(jìn)行編碼。
public class FiberBasedJobRunner implements JobRunner { @Override public long run(List<Job> jobs) { var start = System.nanoTime(); var factory = Thread.builder().virtual().factory(); try (var executor = Executors.newUnboundedExecutor(factory)) { for (Job job : jobs) { executor.submit(job::doIt); } } var end = System.nanoTime(); long timeSpentInMS = Util.nanoToMS(end - start); return timeSpentInMS; } }也許關(guān)于此實(shí)現(xiàn)的第一個(gè)值得注意的事情是它的簡(jiǎn)潔性,如果將其與ThreadBasedJobRunner進(jìn)行比較,您會(huì)發(fā)現(xiàn)該代碼的行數(shù)更少! 主要原因是ExecutorService接口中的新更改現(xiàn)在擴(kuò)展了Autocloseable ,因此,我們可以在try-with-resources范圍中使用它。 所有提交的作業(yè)完成后,將執(zhí)行try塊之后的代碼。
這正是我們用來在JAVA中編寫結(jié)構(gòu)化并發(fā)代碼的主要結(jié)構(gòu)。
上面代碼中的另一件事是我們可以構(gòu)建線程工廠的新方法。 Thread類具有一個(gè)稱為builder的新靜態(tài)方法,可用于創(chuàng)建Thread或ThreadFactory 。
此行代碼正在創(chuàng)建一個(gè)創(chuàng)建虛擬線程的線程工廠。
現(xiàn)在,讓我們看看使用此實(shí)現(xiàn)可以運(yùn)行10,000個(gè)作業(yè)所需的時(shí)間。
執(zhí)行的前10名:
121毫秒
122毫秒 122毫秒 123毫秒 124毫秒 124毫秒 124毫秒 125毫秒 125毫秒 125毫秒
鑒于Project Loom仍在積極開發(fā)中,仍然有提高速度的空間,但結(jié)果確實(shí)很棒。
不論是全部還是部分,許多應(yīng)用都可以以最小的努力受益于Fibers! 唯一需要更改的是線程池的線程工廠 ,就是這樣!
具體來說,在此示例中,應(yīng)用程序的運(yùn)行時(shí)速度提高了約6倍,但是,速度并不是我們?cè)谶@里實(shí)現(xiàn)的唯一目標(biāo)!
盡管我不想寫有關(guān)使用Fibers大大減少了的應(yīng)用程序的內(nèi)存占用的信息,但是我強(qiáng)烈建議您在這里瀏覽本文的代碼,并比較使用的內(nèi)存量和每個(gè)實(shí)現(xiàn)占用的OS線程數(shù)! 您可以在此處下載Loom的官方早期試用版。
在接下來的文章中,我將詳細(xì)介紹Loom引入的其他API項(xiàng)目,以及我們?nèi)绾螌⑵鋺?yīng)用于現(xiàn)實(shí)生活中的用例。
請(qǐng)不要猶豫,通過評(píng)論與我分享您的反饋意見
翻譯自: https://www.javacodegeeks.com/2020/02/openjdk-loom-and-structured-concurrency.html
總結(jié)
以上是生活随笔為你收集整理的OpenJDK织机和结构化并发的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 截图快捷键什么好用(哪个快捷键截图)
- 下一篇: Jakarta EE贡献–入门