Java SE 19 虛擬線程
作者:Grey
原文地址:
說明
虛擬線程(Virtual Threads)是在Project Loom中開發的,並從 Java SE 19 開始作爲預覽功能引入 JDK。
在線程模型下,一個 Java 線程相當於一個操作系統線程,而這些線程是很消耗資源的,如果啓動的線程過多,會給整個系統的穩定性帶來風險。
虛擬線程解決了這個問題,從 Java 代碼的角度來看,虛擬線程感覺就像普通的線程,但它們不是 1:1
地映射到操作系統線程上。
有一個所謂的載體線程池,一個虛擬線程被臨時映射到該池中。一旦虛擬線程遇到阻塞操作,該虛擬線程就會從載體線程中移除,而載體線程可以執行另一個虛擬線程(新的或之前被阻塞的)。
所以阻塞的操作不再阻塞執行的線程。這使得我們可以用一個小的載體線程池來並行處理大量的請求。
示例
場景
啓動 1000 個任務,每個任務等待一秒鐘(模擬訪問外部API),然後返回一個結果(在這個例子中是一個隨機數)。
任務類如下
package git.snippets.vt;
import java.util.concurrent.Callable;
import java.util.concurrent.ThreadLocalRandom;
/**
* @author <a href="mailto:[email protected]">Grey</a>
* @date 2022/9/21
* @since 19
*/
public class Task implements Callable<Integer> {
private final int number;
public Task(int number) {
this.number = number;
}
@Override
public Integer call() {
System.out.printf("Thread %s - Task %d waiting...%n", Thread.currentThread().getName(), number);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
System.out.printf("Thread %s - Task %d canceled.%n", Thread.currentThread().getName(), number);
return -1;
}
System.out.printf("Thread %s - Task %d finished.%n", Thread.currentThread().getName(), number);
return ThreadLocalRandom.current().nextInt(100);
}
}
接下來,我們測試使用線程池開啓 100 個線程處理 1000 個任務需要多長時間。
package git.snippets.vt;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
/**
* @author <a href="mailto:[email protected]">Grey</a>
* @date 2022/9/21
* @since 19
*/
public class App {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService executor = Executors.newFixedThreadPool(100);
List<Task> tasks = new ArrayList<>();
for (int i = 0; i < 1_000; i++) {
tasks.add(new Task(i));
}
long time = System.currentTimeMillis();
List<Future<Integer>> futures = executor.invokeAll(tasks);
long sum = 0;
for (Future<Integer> future : futures) {
sum += future.get();
}
time = System.currentTimeMillis() - time;
System.out.println("sum = " + sum + "; time = " + time + " ms");
executor.shutdown();
}
}
運行結果如下
Thread pool-1-thread-1 - Task 0 waiting...
Thread pool-1-thread-3 - Task 2 waiting...
Thread pool-1-thread-2 - Task 1 waiting...
……
sum = 49879; time = 10142 ms
接下來,我們用虛擬線程測試整個事情。因此,我們只需要替換這一行
ExecutorService executor = Executors.newFixedThreadPool(100);
替換爲
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
執行效果如下
Thread - Task 0 waiting...
Thread - Task 2 waiting...
Thread - Task 3 waiting...
……
sum = 48348; time = 1125 ms
1125 ms
VS 10142 ms
,性能提升非常明顯。
注:本示例需要在 JDK 19 下運行,且需要增加 --enable-preview
參數,在 IDEA 下,這個參數配置如下
我們已經瞭解了創建虛擬線程的一種方法:使用Executors.newVirtualThreadPerTaskExecutor()
創建的執行器服務,爲每個任務創建一個新的虛擬線程。
使用Thread.startVirtualThread()
或Thread.ofVirtual().start()
,我們也可以明確地啓動虛擬線程。
Thread.startVirtualThread(() -> {
// code to run in thread
});
Thread.ofVirtual().start(() -> {
// code to run in thread
});
特別說明:Thread.ofVirtual()
返回一個VirtualThreadBuilder
,其start()
方法啓動一個虛擬線程。另一個方法Thread.ofPlatform()
返回一個PlatformThreadBuilder
,通過它我們可以啓動一個平臺線程。
這兩種構造方法都實現了Thread.Builder
接口。這使得我們可以編寫靈活的代碼,在運行時決定它應該在虛擬線程還是平臺線程中運行。