OpenJDK织机和结构化并发
Project Loom是Hotspot Group贊助的項目之一,旨在向JAVA世界提供高吞吐量和輕量級的并發(fā)模型。 在撰寫本文時,Loom項目仍處于積極開發(fā)中,其API可能會更改。
為什么要織機?
每個新項目可能會出現的第一個問題是為什么?
為什么我們需要學習新的東西,它對我們有幫助? (如果確實如此)
因此,要專門針對Loom回答這個問題,我們首先需要了解JAVA中現有線程系統如何工作的基礎知識。
JVM內部產生的每個線程在OS內核空間中都有一個一對一的對應線程,并具有自己的堆棧,寄存器,程序計數器和狀態(tài)。 每個線程的最大部分可能是堆棧,堆棧大小以兆字節(jié)為單位,通常在1MB到2MB之間。
因此,這些類型的線程在啟動和運行時方面都很昂貴。 不可能在一臺機器上產生1萬個線程并期望它能正常工作。
有人可能會問為什么我們甚至需要那么多線程? 鑒于CPU只有幾個超線程。 例如,CPU Internal Core i9總共有16個線程。
嗯,CPU并不是您的應用程序使用的唯一資源,任何沒有I / O的軟件都只會導致全球變暖!
一旦線程需要I / O,OS就會嘗試為其分配所需的資源,并同時調度另一個需要CPU的線程。 因此,我們在應用程序中擁有的線程越多,我們就越可以并行利用這些資源。
一個非常典型的示例是Web服務器。 每臺服務器都能在每個時間點處理數千個打開的連接,但是同時處理那么多連接要么需要數千個線程,要么需要異步非阻塞代碼( 我可能會在接下來的幾周內撰寫另一篇文章,以解釋更多有關異步代碼 ),就像前面提到的,成千上萬個OS線程既不是您也不是OS會滿意的!
織機如何提供幫助?
作為Project Loom的一部分,引入了一種稱為Fiber的新型線程。 光纖也稱為虛擬線程 , 綠色線程或用戶線程,因為這些名稱暗示完全由VM處理,并且OS甚至都不知道此類線程存在。 這意味著并非每個VM線程都需要在OS級別具有相應的線程! 虛擬線程可能被I / O阻塞,或者等待從另一個線程獲取信號,但是,與此同時,其他虛擬線程也可以利用基礎線程!
上圖說明了虛擬線程和OS線程之間的關系。 虛擬線程可以簡單地被I / O阻塞,在這種情況下,基礎線程將被另一個虛擬線程使用。
這些虛擬線程的內存占用量將以千字節(jié)為單位,而不是兆字節(jié)。 如果需要,可以在生成它們之后擴展它們的堆棧,這樣JVM不需要為它們分配大量內存。
因此,既然我們已經有了一種非常輕巧的方式來實現并發(fā),我們就可以重新考慮存在于Java經典線程中的最佳實踐。
如今,用于在Java中實現并發(fā)的最常用的構造是ExecutorService的不同實現。 它們具有非常方便的API,并且相對易于使用。 執(zhí)行程序服務具有一個內部線程池,用于根據開發(fā)人員定義的特征來控制可以產生多少個線程。 該線程池主要用于限制應用程序創(chuàng)建的OS線程的數量,因為如上所述,它們是昂貴的資源,我們應該盡可能地重用它們。 但是現在可以生成輕量級虛擬線程了,我們也可以重新考慮使用ExecutorServices的方式。
結構化并發(fā)
結構化并發(fā)是一種編程范式,是一種編寫易于讀取和維護的并發(fā)程序的結構化方法。 如果代碼對并發(fā)任務有明確的入口和出口點,則其主要思想與結構化編程非常相似,與啟動可能比當前作用域持續(xù)時間更長的并發(fā)任務相比,對代碼的推理要容易得多!
為了更清楚地了解結構化并發(fā)代碼的外觀,請考慮以下偽代碼:
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方法應該通過電子郵件和SMS通知用戶,并且一旦成功完成此方法將記錄一條消息。 使用結構化并發(fā),可以保證在兩種通知方法完成后立即寫入日志。 換句話說,如果嘗試范圍在其中所有已啟動的并發(fā)作業(yè)都完成了,那么它將完成!
注意:為了使示例簡單,我們假設notifyByEmail和notifyBySMS在上面的示例中,在內部確實處理所有可能的極端情況,并始終使其通過。
JAVA的結構化并發(fā)
在本節(jié)中,我將通過一個非常簡單的示例展示如何用JAVA編寫結構化并發(fā)應用程序以及Fibers如何幫助擴展應用程序。
我們要解決的問題
想象一下,所有I / O綁定有1萬個任務,而每個任務恰好需要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; } }在此嘗試中,我們沒有應用Loom項目中的任何內容。 只是一個緩存的線程池,以確保將使用空閑線程,而不是創(chuàng)建新線程。
讓我們看看使用此實現可以運行10,000個作業(yè)所需的時間。 我使用下面的代碼來查找運行速度最快的10個代碼。 為簡單起見,未使用任何微基準測試工具。
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)) ); } }我的機器上的結果是:
執(zhí)行的前10名:
694毫秒
695毫秒 696毫秒 696毫秒 696毫秒 697毫秒 699毫秒 700毫秒 700毫秒 700毫秒
到目前為止,我們有一個代碼,最好情況下大約需要700毫秒才能在我的計算機上運行10,000個作業(yè)。 讓我們這次使用Loom功能實現JobRunner。
第二次嘗試(使用光纖)
在使用Fibers或Virtual Threads的實現中,我還將以結構化的方式對并發(fā)進行編碼。
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; } }也許關于此實現的第一個值得注意的事情是它的簡潔性,如果將其與ThreadBasedJobRunner進行比較,您會發(fā)現該代碼的行數更少! 主要原因是ExecutorService接口中的新更改現在擴展了Autocloseable ,因此,我們可以在try-with-resources范圍中使用它。 所有提交的作業(yè)完成后,將執(zhí)行try塊之后的代碼。
這正是我們用來在JAVA中編寫結構化并發(fā)代碼的主要結構。
上面代碼中的另一件事是我們可以構建線程工廠的新方法。 Thread類具有一個稱為builder的新靜態(tài)方法,可用于創(chuàng)建Thread或ThreadFactory 。
此行代碼正在創(chuàng)建一個創(chuàng)建虛擬線程的線程工廠。
現在,讓我們看看使用此實現可以運行10,000個作業(yè)所需的時間。
執(zhí)行的前10名:
121毫秒
122毫秒 122毫秒 123毫秒 124毫秒 124毫秒 124毫秒 125毫秒 125毫秒 125毫秒
鑒于Project Loom仍在積極開發(fā)中,仍然有提高速度的空間,但結果確實很棒。
不論是全部還是部分,許多應用都可以以最小的努力受益于Fibers! 唯一需要更改的是線程池的線程工廠 ,就是這樣!
具體來說,在此示例中,應用程序的運行時速度提高了約6倍,但是,速度并不是我們在這里實現的唯一目標!
盡管我不想寫有關使用Fibers大大減少了的應用程序的內存占用的信息,但是我強烈建議您在這里瀏覽本文的代碼,并比較使用的內存量和每個實現占用的OS線程數! 您可以在此處下載Loom的官方早期試用版。
在接下來的文章中,我將詳細介紹Loom引入的其他API項目,以及我們如何將其應用于現實生活中的用例。
請不要猶豫,通過評論與我分享您的反饋意見
翻譯自: https://www.javacodegeeks.com/2020/02/openjdk-loom-and-structured-concurrency.html
總結
以上是生活随笔為你收集整理的OpenJDK织机和结构化并发的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 截图快捷键什么好用(哪个快捷键截图)
- 下一篇: java 8 lambda_玩Java