CodeGym /課程 /JAVA 25 SELF /使用虛擬執行緒的 Executor

使用虛擬執行緒的 Executor

JAVA 25 SELF
等級 57 , 課堂 3
開放

1. 重點速覽

你已經對 java.util.concurrent 套件中的類別有些認識,尤其是 ExecutorService。它就像一個「任務管理器」:你把工作提交給它(例如透過 submit()),它會自行決定何時以及用哪個執行緒來執行。一般情況下,底層是固定大小的執行緒池,能節省資源,不會為每個任務都建立新執行緒。

不過,有了虛擬執行緒就不同了!現在你可以奢侈地做到:每個任務一個執行緒,而且不用擔心 JVM 會因為「吃太多」而撐爆。

新方式:Executors.newVirtualThreadPerTaskExecutor()

在 Java 21 中新增了一種建立 ExecutorService 的方式,它會讓每個任務在獨立的虛擬執行緒中執行:

ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();

重要差異:

  • 舊式執行緒池(Executors.newFixedThreadPoolExecutors.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;詳情見下一堂課)。

留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION