虛擬線程的引入與優勢
在Loom項目之前,Java虛擬機(JVM)中的線程是通過java.lang.Thread類型來實現的,這些線程被稱為平臺線程。
然而,平臺線程的創建和維護在資源使用上存在顯著的開銷。首先,創建成本不菲,因為每當操作系統需要創建一個新的平臺線程時,它必須分配大量的內存(通常以兆字節計)來存儲線程的上下文信息、本機棧和Java調用棧。這一過程受到固定大小堆棧的限制,導致創建和調度平臺線程時的開銷在空間和時間上都相當巨大。此外,當調度器需要從當前執行的線程中搶占時,必須處理大量內存的移動,這進一步增加了操作的復雜性和成本。這種開銷不僅限制了可以同時創建的線程數量,而且也容易導致內存資源的耗盡。以下是一個示例,展示了在Java中如何通過不斷實例化新的平臺線程,迅速達到內存耗盡的情況:
private static void stackOverflowErrorDemo() { try { int threadCount = 0; // 嘗試創建高達百萬級的線程數量 while (threadCount++ < 100000000) { // 創建并啟動一個新線程 Thread thread = new Thread(() -?> { try { // 線程休眠1秒,模擬長時間運行的任務 Thread.sleep(Duration.ofSeconds(1)); } catch (InterruptedException e) { // 如果線程被中斷,將其轉換為運行時異常 throw new RuntimeException(e); } }); // 啟動線程 thread.start(); } } catch (RuntimeException e) { // 捕獲并處理由線程啟動過程中可能拋出的運行時異常 e.printStackTrace(); } }
在實際操作中,達到OutOfMemoryError的時間會根據操作系統和硬件的不同而有所差異。然而,通常情況下,這個過程可以在極短的時間內完成。
為了解決這些問題,虛擬線程應運而生。
虛擬線程的優勢
資源效率:虛擬線程在內存使用上更為高效,初始內存占用通常只有幾百字節,遠小于平臺線程所需的幾兆字節。
簡化線程管理:虛擬線程的創建和管理過程更為簡便,通過工廠方法可以輕松創建,無需手動管理線程資源。
避免線程爆炸:由于資源消耗低,虛擬線程可以處理大量并發任務,而不必擔心資源耗盡。
協作調度:虛擬線程采用協作調度模型,減少了鎖競爭和上下文切換的開銷,提升了多線程程序的性能。
避免阻塞:虛擬線程在遇到阻塞操作時可以釋放執行權,允許其他線程執行,提高了程序的響應性。
虛擬線程如何創建
創建虛擬線程是Java中的一項新特性,它旨在解決傳統平臺線程所面臨的資源限制問題。虛擬線程作為java.lang.Thread的一個替代實現,其獨特之處在于將線程的調用堆棧存儲在Java堆內存中,而不是傳統的本地線程堆棧中。這種方式顯著減少了每個線程所需的初始內存占用,通常僅為幾百字節,而不是幾兆字節。更進一步,虛擬線程的堆棧大小是動態可變的,這使得我們無需為各種用例預分配大量內存。以下是創建虛擬線程的兩種方法:
使用工廠方法創建虛擬線程
通過java.lang.Thread的ofVirtual靜態工廠方法,我們可以輕松創建虛擬線程。首先,定義一個輔助函數來創建并啟動一個帶有指定名稱的虛擬線程:
private static Thread createVirtualThread(String name, Runnable runnable) { return Thread.ofVirtual() .name(name) .start(runnable); }
使用ThreadPerTaskExecutor創建虛擬線程
另一種方法是使用專為虛擬線程設計的java.util.concurrent.ExecutorService實現,即ThreadPerTaskExecutor。這個執行器為提交的每個任務創建一個新的虛擬線程:
@SneakyThrows static void createVirtualThreadUsingExecutorsWithName() { final ThreadFactory factory = Thread.ofVirtual().name("worker-", 0).factory(); try (var executor = Executors.newThreadPerTaskExecutor(factory)) { var cleanTime = executor.submit( () -> { log.info("我要打掃衛生"); sleep(Duration.ofMillis(500L)); log.info("衛生打掃完了"); }); var boilingWater = executor.submit( () -> { log.info("我要去燒一些水"); sleep(Duration.ofSeconds(1L)); log.info("水燒好了"); }); cleanTime.get(); boilingWater.get(); } }
在這個示例中,我們使用了submit方法來啟動虛擬線程,它需要一個Runnable或Callable任務。submit方法返回一個Future對象,該對象可以用來跟蹤和控制虛擬線程的執行。
虛擬線程的啟動和同步
與平臺線程相比,虛擬線程的啟動和同步方式略有不同,因為它們是通過ExecutorService來管理的。每個submit調用都返回一個Future對象,這允許我們跟蹤任務的狀態,甚至在必要時阻塞當前線程直到虛擬線程完成其任務。
虛擬線程的原理
??
如上圖所示展示虛擬線程與平臺線程之間的關系:
JVM維護了一個由專用ForkJoinPool創建和維護的平臺線程池。最初,平臺線程的數量等于CPU核心的數量,最多不能超過256個。
對于每個創建的虛擬線程,JVM都會將其執行調度到一個平臺線程上,臨時將虛擬線程的堆棧塊從堆復制到平臺線程的堆棧中。我們說平臺線程變成了虛擬線程的載體線程。
我們可以通過運行使用ThreadPerTaskExecutor創建虛擬線程的用例,觀察其中的一條日志來說明執行過程:
10:30:35.390 [worker-1] INFO in.rcard.virtual.threads.App - VirtualThread[#23,worker-1]/runnable@ForkJoinPool-1-worker-2 | 我要去燒一些水
從日志中進行觀察
1. 線程標識與命名:每個虛擬線程都有一個唯一的標識符和名稱,例如 `VirtualThread[#23,worker-1]`。這里的 `#23` 表示線程的編號,而 `worker-1` 是線程的名稱,它們共同幫助開發者識別和調試線程。
2. 載體線程的分配:虛擬線程執行時,會綁定到一個特定的載體線程(即平臺線程)。例如,`ForkJoinPool-1-worker-2` 表示該虛擬線程正在由默認的ForkJoinPool中的第二個工作線程執行。
3. 阻塞與釋放:當虛擬線程遇到阻塞操作時,其載體線程會被釋放,以便能夠執行其他就緒的虛擬線程。同時,虛擬線程的堆棧塊會從載體線程的堆棧復制回Java堆中,以等待阻塞操作的完成。
4. 再次調度:一旦虛擬線程完成其阻塞操作,調度器會將其重新排入執行隊列。虛擬線程可能會繼續在先前的載體線程上執行,或者根據調度器的決策,在不同的載體線程上繼續執行。
剛才我們提到,默認情況下,JVM會創建與cpu核心數量相等的載體線程(平臺線程),以確保每個物理核心都能被有效利用。那么假如計算機上配備了2個物理核心和通過超線程技術支持的4個邏輯核心,基于此硬件配置,我們可以設計一個程序,該程序旨在生成與邏輯核心數相匹配的虛擬線程數量,即4個虛擬線程。然而,為了探索線程調度的靈活性,我們可以增加一個額外的虛擬線程,使得總數達到5個,即期望5個虛擬線程在4個載體線程上執行,那么至少會有一個載體線程會被重復使用。執行以下程序
static void viewCarrierThreadPoolSize() { final ThreadFactory factory = Thread.ofVirtual().name("worker-", 0).factory(); try (var executor = Executors.newThreadPerTaskExecutor(factory)) { IntStream.range(0, numberOfCores() + 1) .forEach(i -> executor.submit(() -> { log.info("virtual thread number " + i); sleep(Duration.ofSeconds(1L)); })); } }
[worker-0] INFO in.rcard.virtual.threads.App - VirtualThread[#21,worker-0]/runnable@ForkJoinPool-1-worker-1 | virtual thread number 0 [worker-1] INFO in.rcard.virtual.threads.App - VirtualThread[#23,worker-1]/runnable@ForkJoinPool-1-worker-2 | virtual thread number 1 [worker-2] INFO in.rcard.virtual.threads.App - VirtualThread[#24,worker-2]/runnable@ForkJoinPool-1-worker-3 | virtual thread number 2 [worker-4] INFO in.rcard.virtual.threads.App - VirtualThread[#26,worker-4]/runnable@ForkJoinPool-1-worker-4 | virtual thread number 4 [worker-3] INFO in.rcard.virtual.threads.App - VirtualThread[#25,worker-3]/runnable@ForkJoinPool-1-worker-4 | virtual thread number 3
觀察日志,有四個載體線程,分別是ForkJoinPool-1-worker-1、ForkJoinPool-1-worker-2、ForkJoinPool-1-worker-3和ForkJoinPool-1-worker-4,ForkJoinPool-1-worker-4被重復使用了兩次,以上假設正確。
審核編輯 黃宇
-
存儲
+關注
關注
13文章
4296瀏覽量
85799 -
操作系統
+關注
關注
37文章
6801瀏覽量
123283 -
虛擬
+關注
關注
0文章
186瀏覽量
23650 -
虛擬機
+關注
關注
1文章
914瀏覽量
28160 -
線程
+關注
關注
0文章
504瀏覽量
19675
發布評論請先 登錄
相關推薦
評論