1. 重點速覽
你已經對 java.util.concurrent 套件中的類別有些認識,尤其是 ExecutorService。它就像一個「任務管理器」:你把工作提交給它(例如透過 submit()),它會自行決定何時以及用哪個執行緒來執行。一般情況下,底層是固定大小的執行緒池,能節省資源,不會為每個任務都建立新執行緒。
不過,有了虛擬執行緒就不同了!現在你可以奢侈地做到:每個任務一個執行緒,而且不用擔心 JVM 會因為「吃太多」而撐爆。
新方式:Executors.newVirtualThreadPerTaskExecutor()
在 Java 21 中新增了一種建立 ExecutorService 的方式,它會讓每個任務在獨立的虛擬執行緒中執行:
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
重要差異:
- 舊式執行緒池(Executors.newFixedThreadPool、Executors.newCachedThreadPool)會因為作業系統執行緒成本高而限制同時執行的任務數量。
- 新的虛擬 Executor 幾乎不受限:每個任務都會有自己輕量級的虛擬執行緒。
簡單範例
把 10 個任務提交到虛擬 Executor:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class VirtualExecutorDemo {
public static void main(String[] args) {
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
for (int i = 1; i <= 10; i++) {
int taskId = i; // 為 lambda 捕捉變數
executor.submit(() -> {
System.out.println("Task " + taskId + " is running in thread: " +
Thread.currentThread());
});
}
executor.shutdown();
}
}
發生了什麼?
每個任務都會在自己的虛擬執行緒中啟動,你會看到類似下列的輸出:
Task 1 is running in thread: VirtualThread[#24]/runnable@ForkJoinPool-1-worker-1
...
2. 大規模平行:成千上萬個任務——不是問題!
為了感受虛擬執行緒的威力,試著向 ExecutorService 提交不是 10,而是比如說 100_000 個任務。在傳統的執行緒池中,這就像把大象塞進冰箱:JVM 很快就會用完記憶體或變得非常卡頓。有了虛擬執行緒——情況完全不同!
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class VirtualExecutorMassiveDemo {
public static void main(String[] args) {
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
for (int i = 1; i <= 100_000; i++) {
int taskId = i;
executor.submit(() -> {
// 範例中——只睡 1 毫秒
try {
Thread.sleep(1);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
// System.out.println("Task " + taskId + " done."); // 不要印,否則輸出會多到爆!
});
}
executor.shutdown();
}
}
注意:把 100_000 行輸出到螢幕是個壞主意:主控台會比虛擬執行緒更快「被淹沒」。最好不要寫到主控台,或只輸出前幾個任務。
3. newVirtualThreadPerTaskExecutor 是怎麼運作的
簡而言之:這個 ExecutorService 會為你提交的每個任務建立新的虛擬執行緒。不同於固定執行緒池,這裡沒有任務佇列與嚴格的同時執行緒數限制(除了你的 JVM 與硬體的上限)。
在架構上:
- 虛擬執行緒會「映射」到少量作業系統的真實(carrier)執行緒上。
- JVM 會自行決定何時啟動、暫停與恢復哪個虛擬執行緒。
- 如果執行緒被阻塞(例如讀取檔案或等待網路),JVM 可以「凍結」該虛擬執行緒,並釋放 carrier 執行緒去處理其他任務。
4. 範例:用 Future 取得結果
當任務有回傳結果時,ExecutorService 會回傳 Future。其運作方式與一般執行緒完全相同:
import java.util.concurrent.*;
public class VirtualExecutorWithResult {
public static void main(String[] args) throws InterruptedException, ExecutionException {
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
Future<String> future = executor.submit(() -> {
Thread.sleep(500);
return "Hello from virtual thread!";
});
System.out.println("Result: " + future.get()); // 等待結果
executor.shutdown();
}
}
一切都跟以往一樣:可以提交有回傳值的任務,透過 get() 等待結果,而例外也會以標準方式處理。
5. 如何正確關閉 Executor
非常重要的是不要忘記關閉 ExecutorService,以免程式懸掛(即使執行緒是虛擬的,而非「真正的」)。
shutdown() 與 awaitTermination
executor.shutdown(); // 宣告:不再接受新任務
executor.awaitTermination(1, TimeUnit.MINUTES); // 等待所有任務結束(最多 1 分鐘)
為什麼這很重要?
如果不呼叫 shutdown(),虛擬執行緒可能會繼續存活,而程式在執行完 main() 後也不會結束。這是新手常見的錯誤。
6. 實用細節
比較:虛擬 Executor vs 傳統執行緒池
| 傳統執行緒池(newFixedThreadPool) | 虛擬 Executor(newVirtualThreadPerTaskExecutor) | |
|---|---|---|
| 執行緒數量 | 受池大小限制 | 每個任務一條虛擬執行緒,幾乎無上限 |
| 任務佇列 | 會有,如果所有執行緒都忙碌 | 通常沒有:任務會立刻拿到執行緒 |
| 執行緒成本 | 高(堆疊、OS 資源) | 非常低(排程由 JVM 負責) |
| 可擴充性 | 受限 | 幾乎不受限 |
| 適用場景 | CPU-bound 任務、受限的平行度 | I/O-bound 任務、大量平行處理 |
與 Web 伺服器整合
現代的 Web 伺服器(例如 Tomcat、Jetty、Undertow)已經開始支援虛擬執行緒。這代表可以用獨立的虛擬執行緒處理每個 HTTP 請求,而不必擔心在使用者湧入時「被淹沒」。
優點:不必設計複雜的非同步流程與 callback,也不必大量使用 CompletableFuture;程式碼更簡單——可以撰寫熟悉的阻塞式程式碼,但應用依然能擴充。
大規模測試與負載模擬
虛擬執行緒非常適合需要「模擬」成千上萬個同時使用者、請求或操作的測試。例如,發出 10_000 個平行請求到伺服器,每個請求都在自己的虛擬執行緒中。
檔案與網路連線的並行處理
如果應用程式需要處理大量檔案或網路連線,可以讓每個連線在獨立的虛擬執行緒中處理,而不必手動管理執行緒池。
7. 使用虛擬 Executor 時的常見錯誤
錯誤 №1:忘了呼叫 shutdown()。 如果不關閉 Executor,程式不會結束——虛擬執行緒仍然會等待新任務。必要時請加上 awaitTermination(...)。
錯誤 №2:把虛擬執行緒用在重度計算。 虛擬執行緒不會讓完全吃滿 CPU 的任務更快。對 CPU-bound 的情況,最好使用固定執行緒池(Executors.newFixedThreadPool)並仔細調整大小。
錯誤 №3:忽略任務內部的例外。 如果任務丟出例外,它不會傳到主執行緒——請透過 Future(方法 get())處理,或在 lambda 中使用 try/catch。
錯誤 №4:混淆舊語法與新語法/JDK 版本。 請確認你使用正確的 JDK 版本(Java 21+),且 IDE 已設定支援虛擬執行緒。具體的方法是 Executors.newVirtualThreadPerTaskExecutor()。
錯誤 №5:依賴 ThreadLocal 傳遞上下文。 虛擬執行緒經常被建立與銷毀;ThreadLocal 的行為可能與預期不同。要傳遞上下文,請使用 ScopedValue(Scoped Values;詳情見下一堂課)。
GO TO FULL VERSION