1. 實務上如何建立虛擬執行緒
該從理論走向實作了!你已經知道,傳統上如何建立執行緒:
Thread t = new Thread(() -> System.out.println("Hello from thread!"));
t.start();
或者更精簡:
new Thread(() -> System.out.println("Hi!")).start();
而在 Java 21 之後,我們有了新的方式:
Thread.startVirtualThread(() -> System.out.println("Hello from virtual thread!"));
或者更明確地說:
Thread t = Thread.ofVirtual().start(() -> System.out.println("Hello from virtual thread!"));
有什麼差別?
- Thread.ofVirtual().start(...) 會建立虛擬執行緒(Virtual Thread),由 JVM 管理,而非作業系統。
- Thread.ofPlatform().start(...)(或 new Thread(...))—— 傳統的(平台)執行緒,就跟以往一樣。
為什麼這很重要?
你可以建立成千上萬個虛擬執行緒,而不必擔心 OutOfMemoryError。現在,如果你突然決定處理一百萬個請求——Java 會說:「沒問題,再來!」
2. 建立虛擬執行緒的語法
基本範例:
public class VirtualThreadDemo {
public static void main(String[] args) {
Thread thread = Thread.ofVirtual().start(() -> {
System.out.println("來自虛擬執行緒的問候!執行緒: " + Thread.currentThread());
});
// 等待執行緒結束(避免 main 先結束)
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
發生了什麼事?
- 我們透過 Thread.ofVirtual().start(...) 建立一個虛擬執行緒。
- 在執行緒內做了件簡單的事:輸出訊息。
- 最後呼叫 thread.join(),讓主要執行緒等待虛擬執行緒結束(否則程式可能在輸出前就結束)。
請注意:
虛擬執行緒在外觀與行為上幾乎與一般執行緒相同,但內部是 JVM 的魔法!
3. 大量建立虛擬執行緒:Loom 的實戰威力
現在來試試在一般執行緒上很冒險(甚至不可行)的事:建立 10_000 個虛擬執行緒,每個都會輸出自己的編號。
public class VirtualThreadMassive {
public static void main(String[] args) throws InterruptedException {
int N = 10_000;
Thread[] threads = new Thread[N];
for (int i = 0; i < N; i++) {
int threadNum = i;
threads[i] = Thread.ofVirtual().start(() -> {
System.out.println("虛擬執行緒 #" + threadNum + " 正在運作!");
});
}
// 等待所有執行緒結束
for (Thread t : threads) {
t.join();
}
System.out.println("所有虛擬執行緒都已結束!");
}
}
- 對於一般執行緒(new Thread(...)),這樣的程式幾乎保證會因為 OutOfMemoryError 而「掛掉」。
- 對於虛擬執行緒——這是正常模式!JVM 能輕鬆處理成千上萬個執行緒。
順帶一提,如果你覺得 10_000 已經很多,不妨試試 100_000 甚至 1_000_000。在現代機器上,只要你的執行緒做的是簡單工作或等待 I/O,JVM 都能應付。
4. Runnable 與 Lambda:如何把要執行的程式碼交給虛擬執行緒
虛擬執行緒接收任務的方式與一般執行緒相同:透過介面 Runnable。這表示你可以傳遞 lambda、方法參考,或任何實作了 Runnable 的物件。
搭配 lambda 的範例:
Thread.ofVirtual().start(() -> System.out.println("虛擬執行緒中的 lambda!"));
搭配方法的範例:
public class TaskRunner {
public static void main(String[] args) {
Thread.ofVirtual().start(TaskRunner::doWork);
}
static void doWork() {
System.out.println("在虛擬執行緒中工作: " + Thread.currentThread());
}
}
搭配匿名類別的範例:
Thread.ofVirtual().start(new Runnable() {
@Override
public void run() {
System.out.println("虛擬執行緒中的匿名類別!");
}
});
結論:
凡是對一般執行緒有效的做法,對虛擬執行緒同樣適用——只是現在「更輕量、更快速」。
5. 與 ExecutorService 的比較:舊方法與新方法
傳統的 ExecutorService
ExecutorService executor = Executors.newFixedThreadPool(10);
for (int i = 0; i < 100; i++) {
int taskNum = i;
executor.submit(() -> {
System.out.println("任務 #" + taskNum + " 正在執行");
});
}
executor.shutdown();
問題:
如果任務太多而執行緒太少——任務會在佇列中等待。如果執行緒太多——程式會因資源不足而「噎住」。
新方法:以虛擬執行緒為基礎的 Executor
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
for (int i = 0; i < 100_000; i++) {
int taskNum = i;
executor.submit(() -> {
System.out.println("虛擬任務 #" + taskNum);
});
}
executor.shutdown();
這裡發生了什麼?
- 每個任務都會建立一個獨立的虛擬執行緒。
- JVM 會自行管理排程,不會讓系統過載。
- 無需限制池的大小——虛擬執行緒幾乎是「免費」的。
何時使用虛擬執行緒的 Executor?
- 當你有大量任務,且不想煩惱執行緒池大小時。
- 當任務彼此獨立且可並行執行時。
- 當你希望簡化流程:不需要手動管理執行緒。
6. 實務建議:何時該用哪一種
何時直接使用 Thread.ofVirtual().start()?
- 當需要為某個獨立任務建立單一執行緒(例如測試、展示或簡單的實驗)。
- 當執行緒數量不多,且你想手動管理它們。
何時使用 Executors.newVirtualThreadPerTaskExecutor()?
- 當需要大量啟動任務(例如處理大量請求、檔案、網路連線)。
- 當任務彼此獨立且不需要互相協調。
- 當你想把虛擬執行緒整合到既有架構中,而該架構已使用 ExecutorService(例如在 Web 伺服器、任務處理器等)。
建議:
如果不確定——就從以虛擬執行緒為基礎的 Executor 開始。這是最通用、也最現代的方式。
7. 在虛擬執行緒中處理例外
從 try-catch 的角度看,虛擬執行緒就是一般執行緒。如果在你的 Runnable 中發生例外,不會「破壞」整個 JVM,而只會讓該執行緒以錯誤結束。
範例:
Thread t = Thread.ofVirtual().start(() -> {
throw new RuntimeException("出了點差錯!");
});
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("主執行緒繼續執行。");
在 ExecutorService 中:
如果透過 submit 送出任務,可以透過 Future 取得結果,並且在呼叫 get() 時拋出例外:
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
Future<?> f = executor.submit(() -> {
throw new RuntimeException("虛擬任務中發生錯誤");
});
try {
f.get();
} catch (ExecutionException e) {
System.out.println("捕捉到來自虛擬執行緒的錯誤: " + e.getCause());
}
executor.shutdown();
8. 建立虛擬執行緒時的常見錯誤
錯誤 №1:把虛擬執行緒與平台執行緒混為一談。 如果你透過 new Thread(...) 或 Thread.ofPlatform() 建立執行緒,那不是虛擬執行緒。只有 Thread.ofVirtual().start(...) 或 Executors 中的方法才會給你真正的 Virtual Threads。
錯誤 №2:期待對重度計算能加速。 虛擬執行緒不會讓嚴重耗 CPU(CPU-bound)的任務更快。如果你有一百萬個執行緒,每個都在計算圓周率到第一百萬位——JVM 並不會「加速」運算,只會在執行緒之間切換。
錯誤 №3:為每個執行緒保留資源(例如資料庫)。 如果你建立了一百萬個虛擬執行緒,但每個都需要獨立的資料庫連線——資料庫會撐不住。虛擬執行緒適合用在大部分時間都在等待(I/O),而不是依賴受限的外部資源的情境。
錯誤 №4:在重要情況下,沒有等待執行緒結束。 如果主要執行緒比虛擬執行緒先結束——程式可能在結果產出前就結束。請使用 join(),或搭配 ExecutorService 的 shutdown() 與 awaitTermination()。
錯誤 №5:使用與虛擬執行緒不相容的過時函式庫。 有些第三方函式庫可能會在作業系統層級封鎖執行緒,或使用原生同步機制,導致虛擬執行緒的效能受影響。務必檢查相容性。
GO TO FULL VERSION