CodeGym /課程 /JAVA 25 SELF /建立虛擬執行緒:Thread.ofVirtual().start()

建立虛擬執行緒:Thread.ofVirtual().start()

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

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(),或搭配 ExecutorServiceshutdown()awaitTermination()

錯誤 №5:使用與虛擬執行緒不相容的過時函式庫。 有些第三方函式庫可能會在作業系統層級封鎖執行緒,或使用原生同步機制,導致虛擬執行緒的效能受影響。務必檢查相容性。

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