Java21的虛擬執行緒Virtual Thread初體驗

大卫小东(Sheldon)發表於2024-07-17

我們之前使用的是作業系統平臺的執行緒,就稱之為“系統執行緒”吧。虛擬執行緒是JDK維護的,原理跟WebFlux的底層實現差不多,都是工作執行緒分離。

要使用虛擬執行緒,需要使用JDK21以上,包括21。

虛擬執行緒可以建立很多很多

系統執行緒不能輕易建立太多,我們一直被教導建立執行緒是很重的活動。

        for (int i = 0; i < 1_000_000; i++) {
            new Thread(() -> {
                longAdder.increment();
                System.out.println(longAdder.longValue());
                try {
                    Thread.sleep(10000);
                } catch (Exception e) {
                    // deal with e
                }
            }).start();
        }

上面嘗試建立百萬個執行緒,執行緒都會休眠不結束。我用了一個LongAdder記錄我的筆記本能實際建立多少執行緒。結果是4000多個,用了6秒:
image
改成虛擬執行緒就輕鬆成功:

        LongAdder longAdder = new LongAdder();
        for (int i = 0; i < 1_000_000; i++) {
            Thread.ofVirtual().start(() -> {
                longAdder.increment();
                System.out.println(longAdder.longValue());
                try {
                    Thread.sleep(100000);
                } catch (Exception e) {
                    // deal with e
                }
            });
        }

因為虛擬執行緒很輕量,所以不要使用執行緒池,可以很輕易的建立很多個。因為能建立很多,所以也不要使用 Thread Local 變數。

IO操作不好阻塞虛擬執行緒的使用

使用系統執行緒,必須透過執行緒池來處理多個任務,不然問題很嚴重:

    static void callService(String taskName) {
        try {
            System.out.println(Thread.currentThread() + " executing " + taskName);
            new URL("自己寫一個http介面?sleep=2000").getContent();
            System.out.println(Thread.currentThread() + " completed " + taskName);

        } catch (Exception e) {
            // deal with e
        }
    }
	
try (ExecutorService executor = Executors.newFixedThreadPool(5)) {
	for (int i = 0; i <= 10; i++) {
		String taskName = "Task" + i;
		executor.execute(() -> callService(taskName));
	}
}

執行的時候你能看到,執行緒在執行結束前需要空閒等待任務的IO。畢竟每個任務都是在某一個執行緒上執行 —— 說這個幹啥?
看一下虛擬執行緒

        try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
            for (int i = 0; i <= 600; i++) {
                String taskName = "Task" + i;
                executor.execute(() -> callService(taskName));
            }
        }

這裡建立了一個虛擬執行緒工廠(而不是執行緒池,記住不要使用虛擬化的執行緒池),它會給每個任務建立新的虛擬執行緒。
程式啟動後,會立即列印600個"executing",而不是像系統執行緒那樣只列印5個。
為了方便,我們少用幾個任務來實驗看一下輸出:

VirtualThread[#50]/runnable@ForkJoinPool-1-worker-2 executing Task1
VirtualThread[#48]/runnable@ForkJoinPool-1-worker-1 executing Task0
VirtualThread[#51]/runnable@ForkJoinPool-1-worker-3 executing Task2
VirtualThread[#51]/runnable@ForkJoinPool-1-worker-2 completed Task2
VirtualThread[#48]/runnable@ForkJoinPool-1-worker-3 completed Task0
VirtualThread[#50]/runnable@ForkJoinPool-1-worker-1 completed Task1

仔細看,這裡一共3個虛擬執行緒,因為工廠建立了三個,根據任務數來的。
但是每個任務都是在兩個虛擬執行緒上:Task1 被worker-2接收,卻被worker-1完成。

啥時候用

關鍵問題來了,我們總該使用虛擬執行緒嗎?
對各種問題都通用的答案是:你沒遇到問題就別想著解決問題。
如果的確有問題,想看看虛擬執行緒是否合適,可以看一下任務是否是IO密集型的。
對於計算密集型任務,系統執行緒比虛擬執行緒有效得多。
虛擬執行緒跟WebFlux一樣,只能提升系統的吞吐量,並不能加快單個任務的完成時間。

相關文章