Java19虛擬執行緒都來了,我正在寫的執行緒程式碼會被淘汰掉嗎?
Java19中引入了虛擬執行緒,雖然默認是關閉的,但是可以以Preview模式啟用,這絕對是一個重大的更新,今天Java架構雜談帶大家開箱驗貨,看看這傢伙實現了什麼了不起的功能。
1 為什麼需要虛擬執行緒?
小張貪小便宜,在路邊攤花一塊錢買了一籠熱氣騰騰的小籠包,下肚之後肚子疼得不行,於是在公司找坑位。掃了幾層樓,沒找到一個坑位,坑裡面的人要麼在抽煙,要麼在外放刷影片、要麼腸道不是很順暢,蹲了半天沒拉出來。小張很鄙視在坑位裡面不幹正事的行為,此刻,與小張一同排隊等坑位的還有幾個同事…
小張突然感受到了從菊花傳來的一股無法壓制的推力,像極了JVM發生OOM前一刻的癥狀。在這千鈞一髮的時刻,小張爆發了。
他把在廁所裡面抽煙刷影片拉不出來的人全部都趕出來了,急著釋放記憶體的同事立刻進行解決了,然後趁味道還沒消散,立刻再讓出坑位把抽煙的人趕進去接著抽。
坑位就是作業系統的執行緒,以前一個同事蹲坑之後,就佔了坑位了,其他同事無法使用。而用了虛擬執行緒後,誰要是在廁所裡面刷影片、抽煙就會被趕出來,避免佔用資源,這就是虛擬執行緒的好處。
虛擬執行緒在Project Loom項目中已經孵化很久了,現在 Project Loom 的JEP 425: 虛擬執行緒可以在Java 19中以預覽的方式使用了,接下來Java架構雜談就帶大家深入地了解一下它。
在一個高並發系統中,給每個請求創建一個執行緒來處理,是很不可取的,Java執行緒對應一個作業系統執行緒,而作業系統執行緒資源是比較寶貴的。但是如果沒有開啟這麼多執行緒,又無法處理這麼多請求,特別是遇到一些鎖、IO、系統調用等操作時,需要更長的時間來處理請求。我們一般的的做法是引入執行緒池,但是執行緒池也是有限制的,假設以下場景:
執行緒池設置為100個執行緒,一個請求需要花費兩秒,而大部分時間都花在了IO上,那麼每秒最多可以響應50個請求。此時,CPU可能利用率很低,因為系統需要花費大部分時間來執行IO等待。
我們以往只能通過各種響應式框架來克服這個問題。但是引入了響應式編程框架後,程式碼將會變得越來越複雜,看看以下SpringCloud Gateway的源碼,當你想調試它時,你會感到抓狂:
@Override
public Mono<Void> handle(ServerWebExchange exchange) {
if (this.handlerMappings == null) {
return createNotFoundError();
}
if (CorsUtils.isPreFlightRequest(exchange.getRequest())) {
return handlePreFlight(exchange);
}
return Flux.fromIterable(this.handlerMappings)
.concatMap(mapping -> mapping.getHandler(exchange))
.next()
.switchIfEmpty(createNotFoundError())
.flatMap(handler -> invokeHandler(exchange, handler))
.flatMap(result -> handleResult(exchange, result));
}
為了使用響應式編程,你不僅需要試圖接受這些難以閱讀的程式碼,而且資料庫程式和其他外部服務的驅動程式也必須支援響應式模式。
現在,有了虛擬執行緒,你可以不用寫這種反人類的程式碼了。
接下來,Java架構雜談帶您深入淺出徹底弄懂虛擬執行緒是怎麼回事。
2 什麼是虛擬執行緒?
通過使用虛擬執行緒,可以讓我們繼續編寫易於閱讀和可維護性好的高性能程式碼。從Java的角度來看,虛擬執行緒跟普通的執行緒沒有什麼區別,但是虛擬執行緒與作業系統執行緒並不是一對一的關係。
2.1 虛擬執行緒模型
虛擬執行緒有一個所謂的載體執行緒池,虛擬執行緒臨時映射到該執行緒池上,一旦虛擬執行緒遇到阻塞操作,虛擬執行緒就從載體執行緒中移除,然後載體執行緒就可以執行其他新的虛擬執行緒或者之前阻塞後恢復的虛擬執行緒了。
載體執行緒與虛擬執行緒的關係如下圖所示,一個載體執行緒上面可以運行很多虛擬執行緒,每當虛擬執行緒變為非Runnable狀態時,就從載體執行緒上卸載:
可以看到,虛擬執行緒中的阻塞操作不在阻塞正在執行的執行緒,這允許我們使用少量的載體執行緒並行處理大量的請求。
虛擬執行緒的載體執行緒是
ForkJoinPool
在 FIFO 模式下運行的執行緒。此池的大小默認為可用處理器的數量,可以使用系統屬性jdk.virtualThreadScheduler.parallelism
進行調整。將來,可能會有更多選項來創建自定義調度程式。請注意:此
ForkJoinPool
不同於 [common pool](//docs.oracle.com/en/java/javase/18/docs/api/java.base/java/util/concurrent/ForkJoinPool.html #commonPool()),在並行流的實現中就使用到了common pool,此pool是在 LIFO 模式下運行的。
2.2 執行緒
執行緒是Java的基礎。當我們運行一個Java程式時,它的main方法作為第一個棧幀被調用。當一個方法調用另一個方法時,被調用者與調用者在同一個執行緒上運行,返回資訊記錄到執行緒堆棧上。方方法使用局部變數時,它們存儲在執行緒堆棧的方法調用棧幀中。
當程式出現問題時,我們可以通過遍歷當前執行緒堆棧來進行跟蹤。
執行緒是Java程式調度的基本單位。當執行緒阻塞等待磁碟IO、網路IO或者鎖時,該執行緒被掛起,以便另一個執行緒可以在CPU上運行。構建在執行緒之上的異常處理、單步調試和分析、順序控制流和局部變數等已經成為了編碼中使用率非常高的東西。執行緒是Java並發模型的基礎。
2.2.1 平台執行緒
在進入虛擬執行緒的世界之前,我需要重新審視經典執行緒,我們可以將之稱為平台執行緒。
常見的Java執行緒模式實現方式有:
- 使用內核執行緒實現;
- 使用用戶執行緒實現;
- 使用用執行緒+輕量級進程混合實現;
JVM規範並沒有限定Java執行緒需要那種模型,對於Windows和Linux版本使用的是1:1的執行緒,映射到輕量級進程中(每個輕量級進程都需要有一個內核執行緒支援)。
詳細閱讀:Java架構雜談的另一篇文章:一文帶你徹底理解同步和鎖的本質(乾貨)
由於大多數作業系統的實現方式決定了創建執行緒的代價比較昂貴,因此系統能夠創建的執行緒數量收到了限制,最終導致我們在程式中使用執行緒的方式產生了影響。
執行緒棧大小的限制
作業系統通常在創建執行緒時將執行緒堆棧分配為單片記憶體塊,一旦分配完成,無法調整大小。為此我們需要根據實際情況手動設置一個固定的執行緒棧大小。
如果執行緒棧被配置的過大,我們將會需要使用更多的記憶體,如果配置的太小,很容易就觸發StackOverflowException。為了避免StackOverflowException,一般的我們傾向於預留多點執行緒棧,因為消耗多點記憶體總比程式報錯要好。但這樣會限制我們在給定的記憶體量的情況下可以擁有的並發執行緒數量。
而限制可以創建的執行緒數量又會帶來並發的問題。因為伺服器應用程式一般的處理方法是每個請求分配一個執行緒( thread-per-request)
。這種處理將應用程式的並發單元(任務)與平台(執行緒)對齊,可以最大限度的簡化開發、調試和維護的複雜度,同時無形的帶來很多好處,如程式執行的順序錯覺(比起非同步框架,好處太明顯了)。在這種模式下,開發人員需要很少的並發意識,因為大多數請求是相互獨立的。
也許這種模式可以輕鬆的為1000個並發請求提供服務,但是卻無法支撐100萬並發的請求,即使具有足夠好的CPU和足夠大的IO頻寬。
擴展閱讀,如何讓伺服器支援更高的並發:網路編程範式:高性能伺服器就這麼回事 | C10K,Event Loop,Reactor,Proactor
為了讓伺服器支援更大的並發請求,Java開發人員只能選擇以下幾個比較糟糕的選擇:
- 限制程式碼的編寫方式,使其可以使用更小的堆棧大小,這迫使我們方式大多數第三方庫;
- 投入更多的硬體,或者切換到Reactor或者Proactor編程風格。雖然非同步模型最近幾年有些流行,前幾年就聽說有同學的公司在項目裡面推非同步編程框架,但是這樣意味著我們必須以高度受限的風格進行編碼,這要求我們放棄執行緒給我們帶來的許多好處,例如斷點調試,堆棧跟蹤等。最終會導致犧牲了Java程式語言原本具有的一些優勢。
2.2.2 虛擬執行緒
虛擬執行緒則是一種執行緒的替代實現。在這種實現中,執行緒棧幀存儲在Java的堆記憶體中,而不是存儲在作業系統分配到單片記憶體塊中。我們再也不需要去猜測一個執行緒可能需要多少棧空間,虛擬執行緒佔用的記憶體開始時只有幾百位元組,隨著調用堆棧的擴展和收縮而自動擴展和收縮,這使得系統具有了更好的可伸縮性。
對於作業系統來說,仍然只知道平台執行緒,它是基本的調度單元,虛擬執行緒是在JVM中實現的,Java運行虛擬執行緒時通過將其安裝在某個平台執行緒(稱為載體執行緒)上來運行它。掛載虛擬執行緒到平台執行緒的時候,JVM會將所需的執行緒棧從堆中臨時複製到載體執行緒堆棧中,並在掛載時借用載體堆棧。
當在虛擬執行緒中運行的程式碼因為IO、鎖或者其他資源可用性而阻塞時,虛擬執行緒可以從載體執行緒中卸載,並且複製的任何執行緒棧改動資訊都將會存回到堆中,從而釋放載體執行緒,以使其繼續運行其他虛擬執行緒。
JDK中幾乎所有的阻塞掉都已經調整過了,因此當在虛擬執行緒上遇到阻塞操作時,虛擬執行緒會從其載體上卸載而不是阻塞。
例如,在LockSupport中,要park執行緒的時候,做了虛擬執行緒的兼容處理:
在Thread的sleep方法中,也做了兼容處理:
JDK中幾乎所有的阻塞點,都做了虛擬執行緒判斷,並且會卸載虛擬執行緒而不是阻塞它。
虛擬執行緒對編寫多執行緒程式有影響嗎?
在載體執行緒上掛載和卸載虛擬現在是JVM內部的處理邏輯,在Java程式碼層面是完全不可見的。Java程式碼無法觀察到當天載體的身份,也就是說,調用Thread.currentThtread總是返回虛擬執行緒。
載體執行緒的ThreadLocal值對已掛載的虛擬執行緒不可見,載體執行緒的執行緒棧幀不會出現在虛擬執行緒的異常或者執行緒轉儲中。
在虛擬執行緒的生命周期中,可能在許多不同的載體執行緒上運行。
如何創建虛擬執行緒
虛擬執行緒具有相對較少的新的API,創建完虛擬執行緒後,他們是普通的Thread對象,並且表現得向我們已經所了解的執行緒。例如,Thread.currentThread、ThreadLocal、中斷、堆棧遍歷等,在虛擬執行緒上的工作方式與在平台執行緒上的工作方式完全相同,這意味著我們可以自信地在虛擬執行緒上運行我們現有的程式碼。
虛擬執行緒不就是綠色執行緒嗎?
在電腦程式設計中,綠色執行緒是一種由運行環境或虛擬機調度,而不是由本地底層作業系統調度的執行緒。綠色執行緒並不依賴底層的系統功能,模擬實現了多執行緒的運行,這種執行緒的管理調配發生在用戶空間而不是內核空間,所以它們可以在沒有原生執行緒支援的環境中工作。
綠色執行緒名字由來:綠色執行緒的名稱來源於最初的Java執行緒庫。這是因為甲骨文公司的「綠色團隊」最初設計了Java 的執行緒庫。
在Java 1.0時代,一些JVM使用綠色執行緒來實現執行緒。虛擬執行緒與綠色執行緒表面上有許多相似之處,因為他們都是由JVM而不是作業系統管理的。
但是Java 1.0的綠色執行緒仍然有大的單一的堆棧,綠色執行緒是那個時代的產物,當時系統是單核的,作業系統沒有支援線層。而虛擬執行緒與其他語言中的用戶模式執行緒有更多的共同點(例如Go中的goroutine或者Erlang中的進程),而Java的虛擬執行緒的語義同時還擁有與已有的執行緒語義相同的優勢。
3 性能對比
3.1 虛擬執行緒與平台執行緒區別?
下面通過一個案例來對比虛擬執行緒和平台執行緒。
在本例子中,開啟2000個執行緒,每個執行緒休眠1秒,觀察執行情況。通過同樣的環境和硬體配置執行以下程式碼。
3.1.1 平台執行緒
程式碼如下:
package com.itzhai.demo.jdk19;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;
public class ThreadTest implements Callable<Boolean> {
private final int number;
public ThreadTest(int number) {
this.number = number;
}
@Override
public Boolean call() {
System.out.printf("執行緒:%s - 任務:%d sleep...%n", Thread.currentThread().getName(), number);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
System.out.printf("執行緒:%s - 任務:%d canceled...%n", Thread.currentThread().getName(), number);
return false;
}
System.out.printf("執行緒:%s - 任務:%d finished....%n", Thread.currentThread().getName(), number);
return true;
}
public static void main(String[] args) throws InterruptedException, ExecutionException {
Thread.sleep(10000L);
System.out.println("開始執行任務");
ExecutorService executor = Executors.newFixedThreadPool(2_000);
List<ThreadTest> tasks = new ArrayList<>();
for (int i = 0; i < 2_000; i++) {
tasks.add(new ThreadTest(i));
}
long start = System.currentTimeMillis();
List<Future<Boolean>> futures = executor.invokeAll(tasks);
long successCount = 0;
for (Future<Boolean> future : futures) {
if (future.get()) {
successCount ++;
}
}
long end = System.currentTimeMillis();
System.out.println("總共耗時: " + (end - start) + " ms,成功數量:" + successCount);
executor.shutdown();
Thread.sleep(10000L);
}
}
開啟了2016個系統執行緒。
記憶體使用最多達到了接近60M,CPU使用率最多超過了4%。
3.1.2 虛擬執行緒
程式碼如下:
package com.itzhai.demo.jdk19;
import java.time.Duration;
import java.util.concurrent.Executors;
import java.util.stream.IntStream;
public class VirtualThreadTest {
public static void main(String[] args) throws InterruptedException {
Thread.sleep(10000L);
long start = System.currentTimeMillis();
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
IntStream.range(0, 2_000).forEach(i -> {
executor.submit(() -> {
Thread.sleep(Duration.ofSeconds(1));
return i;
});
});
}
long end = System.currentTimeMillis();
System.out.println("總共耗時: " + (end - start));
Thread.sleep(10000L);
}
}
開啟了25個系統執行緒:
記憶體使用最多不到25M,CPU使用率最多1%左右。
對比之下,高下立斷。單單是看這幾個指標差距就已經很明顯了。
3.2 如何創建虛擬執行緒
你可以使用Executors.newVirtualThreadPerTaskExecutor()
為每個任務創建一個新的虛擬執行緒:
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
使用Thread.startVirtualThread()
or Thread.ofVirtual().start()
,我們也可以顯式啟動虛擬執行緒:
Thread.startVirtualThread(() -> {
// code to run in thread
});
Thread.ofVirtual().start(() -> {
// code to run in thread
});
4 虛擬執行緒優勢
4.1 更好的系統伸縮性
一般的伺服器程式中,會存在大量的非活動執行緒,伺服器程式花在網路、文件、或者數據IO上的時間要比實際執行運算要多得多。如果我們在平台執行緒中運行每個任務,大多數時候,執行緒將在IO獲取其他資源的可用性上被阻塞。而虛擬執行緒運行 IO-bound thread-per-task應用程式更好的擺脫最大執行緒數限制的瓶頸,從而提高硬體的利用率。
虛擬執行緒是我們可以即實現硬體的最佳利用率,又可以繼續與平台協調的編程風格繼續編碼,而不是類似非同步編程框架那種與意外編程風格格格不入的方式重寫程式碼。不得不說,Java的虛擬執行緒真的是實現的很不錯。
虛擬執行緒擅長IO密集型任務的擴展性
對於一般的CPU密集型的任務,我們一般會通過fork-join框架和並行流來獲取最佳的CPU利用率,可以很輕鬆的擴展受CPU限制的工作負載。
而對於IO密集型的任務,虛擬執行緒則提供了對應的應對方案,為IO密集型的任務工作提供了擴展性優勢。
虛擬執行緒不是萬能的,與fork-join是互補的關係。
4.2 幹掉醜陋的響應式編程
響應式編程框架是定義了很多API來實現非同步處理,而Java的虛擬執行緒是直接改造了JDK,讓你可以直接使用Java原生API就可以實現響應式編程框架對性能提升的效果,而且不用編寫那些令人頭疼的回調程式碼。
許多響應式框架要求開發人員折中考慮thread-per-request的編程模式,更多的考慮非同步IO、回調、執行緒共享等,來實現更充分的利用硬體資源。在響應式編程模型中,當一個活動要執行IO時,它會啟動給一個非同步操作,該操作將在完成時調用回調函數。框架將在某個執行緒上調用回調函數,但不一定是啟動操作的同一執行緒。這意味著開發人員必須將它們的邏輯分解為IO交換和計算步驟,這些步驟被縫合到一個連續的工作流程中。因為請求旨在計算任務中才使用執行緒,並發請求的數量不受執行緒數量的限制。
響應式編程框架的這種可伸縮性需要付出巨大的代價:你進程不得不放棄開發平台和生態系統的一些基本特性。
在thread-per-request模型中,如果你想按順序執行兩件事情,只需順序編寫即可,其他的如循環、條件或者try-catch塊都是可以直接使用的。但是在非同步風格中,通常無法使用程式語言為你提供的順序組合、迭代或其他功能來構建工作流,必須通過在非同步框架使用特定的API來完成模擬循環和條件等,這絕對不會比語言中內置的結構那樣靈活或熟悉。如果我們使用的是阻塞操作庫,而整個庫還沒有適配非同步工作方式,我們可能也沒法使用這些。總結來說,就是我們可以在響應式編程中獲得可擴展性,但是我們必須放棄使用部分語言特性和生態系統。
這些框架也是我們放棄類許多便捷的Java運行時特性,如堆棧跟蹤、調試器和分析器等,因為請求在每個階段都可能在不同的執行緒中執行,並且服務執行緒可能交錯屬於不同請求的計算。非同步框架的並發單元是非同步管道的一個階段,與平台的並發單元不同。
而虛擬執行緒運行我們在不放棄語言特性和運行時特性的情況活動相同的吞吐量優勢,這正是虛擬執行緒令人著迷的地方。
4.3 阻塞操作將不再掛起內核執行緒
這就跟虛擬執行緒的實現有關了,JDK做了大量的改進,以確保應用程式在使用虛擬執行緒是擁有良好的體驗:
- 新的套接字實現:為了更好的支援虛擬執行緒,需要讓阻塞方法可被中斷,為此使用了JEP 353 (重新實現 Legacy Socket API) and JEP 373 (重新實現舊版 DatagramSocket API)替換了Socket、ServerScoket和DatagramSocket的實現。
- 虛擬執行緒感知:JDK中幾乎所有的阻塞點,都做了虛擬執行緒判斷,並且會卸載虛擬執行緒而不是阻塞它;
- 重新審視ThreadLocal:JDK中的許多ThreadLocal用法都根據執行緒使用模式的預期變化進行了修訂;
- 重新審視鎖:當虛擬執行緒在執行synchronized塊時,無法從載體執行緒中卸載,這會影響系統吞吐量的可伸縮性,如果要避免這種情況,請使用ReentrantLock代替synchronized。有一些跟蹤排查方法可以使用,具體閱讀:JEP 425: Virtual Threads (Preview)#Executing virtual threads;
- 改進的執行緒轉儲:通過使用jcmd,提供了更好的執行緒轉儲,可以過濾掉虛擬執行緒、將相關的虛擬執行緒組合在一起,或者以機器可讀的方式生成轉儲,這些轉儲可以進行後處理以獲得更好的可觀察性。
4.4 虛擬執行緒會取代掉原有的執行緒嗎?
可能很多朋友都會有這個疑問。
在JEP 425: Virtual Threads (Preview)[^1]中,提到了虛擬執行緒的設計目標,同時也提到了Non-Goals(非目標):
- 刪除傳統的執行緒實現,或靜默遷移現有應用程式以使用虛擬執行緒不是目標;
- 改變 Java 的基本並發模型不是目標;
- 在 Java 語言或 Java 庫中提供新的數據並行結構不是目標。Stream API仍然是並行處理大型數據集的首選方式。
虛擬執行緒不替代原有的執行緒,它們是互補的。但是許多伺服器應用程式會選擇虛擬執行緒來實現更大的可擴展性。服務端的程式設計師們也無需多操心,等使用的框架都支援虛擬執行緒的時候,理想的情況下,只需要改動一下框架配置,就完成了虛擬執行緒的切換,也許這個時候,我們可以為開源框架的虛擬執行緒改造做點貢獻。
5 使用虛擬執行緒,請忘掉這些東西
虛擬執行緒與之前的執行緒API沒有什麼差別,為此,使用虛擬執行緒,你需要學習的東西比較少。
但是為了更好的使用虛擬執行緒,你需要忘掉以前的一些東西。
5.1 不再依賴執行緒池
Java 5 引入了java.util.concurrent
包,其中包括了ExecutorService框架,通過使用ExecutorService以策略驅動的方式管理和池化執行緒池通常比直接創建執行緒要好得多。
對於平台執行緒,我們習慣於將它們池化,並且在一些公司的開發規範中,是一種強制措施,以限制資源利用率,否則容易耗盡記憶體,並將執行緒啟動的成本分攤到多個請求上。但是也引入了其他的問題,流例如ThreadLocal污染導致記憶體泄露。
但是在虛擬執行緒面前,池化技術反而成了反模式。因為虛擬執行緒的初始化佔用空間非常小,所以創建虛擬執行緒在時間和記憶體上都比創建平台執行緒開銷小得多,甚至數百萬個虛擬執行緒才使用1G記憶體。如果限制執行緒本身以外的某些資源的並發度,例如資料庫連接,我們可以使用Semaphore來獲取稀缺資源的許可。
虛擬執行緒非常輕量級,即使是短期任務也可以創建虛擬執行緒,而嘗試重用或者回收他們會適得其反。虛擬執行緒的設計也考慮到了短期任務,如HTTP請求或者JDBC查詢。
注意:我們不必放棄使用ExecutorService,依舊可以通過新的工廠方法Executors::newVirtualThreadPerTaskExecutor來獲得一個ExecutorService偽每個任務創建一個新的虛擬執行緒。
5.2 請勿過渡使用ThreadLocal
有時候,使用ThreadLocal來快取分配記憶體開銷大的資源,或者為了避免重複分配常用對象,當系統有幾百個執行緒的時候,這種模式的資源使用通常不會過多,並且比起每次使用都重新分配更高效。但是當有幾百萬個執行緒時,每個執行緒只執行一個任務,但是可能分配了更多的實例,每個實例被重用的機會要小得多,最終會導致消耗更大的性能開銷。
從本文我們可以看到虛擬執行緒給我帶來的諸多好處,它允許我們編寫可讀且可維護的程式碼的同事,不會阻塞作業系統執行緒。
在常見的後端框架支援虛擬執行緒之前,我們還需要耐心的等待一段時間。到時小張急著上廁所的時候再也不用排長隊了。
我精心整理了一份Redis寶典給大家,涵蓋了Redis的方方面面,面試官懂的裡面有,面試官不懂的裡面也有,有了它,不怕面試官連環問,就怕面試官一上來就問你Redis的Redo Log是幹啥的?畢竟這種問題我也不會。
在Java架構雜談
公眾號發送Redis
關鍵字獲取pdf文件:
本文作者: arthinking
部落格鏈接: //www.itzhai.com/articles/virtual-thread-in-java19.html
Java19虛擬執行緒都來了,我正在寫的執行緒程式碼會被淘汰掉嗎?
版權聲明: 版權歸作者所有,未經許可不得轉載,侵權必究!聯繫作者請加公眾號。
Refrences
[1]: JEP 425: Virtual Threads (Preview). Retrieved from //openjdk.org/jeps/425
[2]: Virtual Threads: New Foundations for High-Scale Java Applications. Retrieved from //www.infoq.com/articles/java-virtual-threads/
[3]: Virtual Threads in Java (Project Loom). Retrieved from //www.happycoders.eu/java/virtual-threads/
[4]: 綠色執行緒. Retrieved from //zh.m.wikipedia.org/zh-hans/%E7%BB%BF%E8%89%B2%E7%BA%BF%E7%A8%8B
[5]: Coming to Java 19: Virtual threads and platform threads. Retrieved from //blogs.oracle.com/javamagazine/post/java-loom-virtual-threads-platform-threads
[6]: Difference Between Thread and Virtual Thread in Java. Retrieved from //www.baeldung.com/java-virtual-thread-vs-thread