java 21 - 虚拟线程
Java 21 虚拟线程正式启用, 虚拟线程是 Java 语言中实现的一种轻量级线程,它由 JVM 进行创建以及管理,可以轻松地在一个 Java 程序中运行大量、甚至数百万个虚拟线程,以减少编写、维护和调试高吞吐量并发应用程序的工作量。
# 背景
我们Java Web的领域,底层的servlet都是基于阻塞式IO开发的,那就难免会有CPU资源利用不充分的问题,因为基于一个请求对应一个线程的模型,当线程在进行IO操作时,其线程资源是没有被释放的,相当于此时的CPU处于空闲期间,如果线程的IO操作过于频繁,就会导致线程资源被占满而CPU利用率却极低的情况。
对此我们可以做到web sever的无状态去横向堆硬件,做集群负载均衡之类的,或者是通过优化IO模型、数据结构、缓存、异步去解决这个问题,像IO模型的优化就延伸出来很多反应式编程,比方说这些年应用最广泛的Reactor模型,利用事件分发、单线程、多线程、NIO、主从等。本质思想就是把IO连接管理和任务处理解耦,像Redis、nginx、netty这类框架底层都有用到Reactor模型。
而虚拟线程可以调度被IO阻塞的线程资源,以最简单粗暴方式提高CPU资源的利用率。
# 分类
在 Java 21 以后,线程有两种,一种是平台线程,一种是虚拟线程
private static void test2() throws InterruptedException {
Thread.ofPlatform().start(() -> System.out.println(Thread.currentThread()));
Thread.ofVirtual().start(() -> System.out.println(Thread.currentThread()));
Thread.currentThread().join();
}
2
3
4
5
6
打印
Thread[#32,Thread-0,5,main]
VirtualThread[#33]/runnable@ForkJoinPool-1-worker-1
平台线程:[#32线程编号,线程名称,优先级,线程组]
虚拟线程:VirtualThread线程组,#33虚拟线程编号,ForkJoinPool-1-worker-1所属平台线程
2
3
4
5
- 平台线程:平台线程被实现为操作系统线程的简单包装器,也就是 Java 21 之前我们熟悉的哪些线程。平台线程上运行的 Java 代码,在其底层逻辑上,其实就是运行在操作系统的线程上,并且平台线程在其整个生命周期内都与操作系统线程一一对应,最大线程数也取决于平台的上限,所以一般都需要搭配线程池使用,而不是频繁的创建和销毁。
- 虚拟线程:虚拟线程很像Golang的协程,不依赖于特定的操作系统线程。虽然虚底层仍然在操作系统的线程上运行代码,但与平台线程不相同的是,在平台线程中运行的代码调用阻塞 I/O 操作时,JVM 就会挂起该平台线程(也就会挂起操作系统线程),直到阻塞 I/O 可以恢复为止,而在虚拟线程中调用阻塞 I/O 操作时,JVM 虽然也会挂起该虚拟线程,但是与平台线程不同的是,被挂起虚拟线程关联的操作系统线程是可以为其他虚拟线程继续服务的。
相当于虚拟线程不像平台线程由操作系统去调度,而是交给JVM去调度,有很高的弹性,创建销毁的成本也很低,也能降低线程上下文切换的开销。

# 原理

虚拟线程启动的时候会自动与平台线程绑定,相当于一个任务队列,当我们使用虚拟线程去执行IO操作被阻塞时,如果当前平台线程没有其他虚拟线程任务了,则会从其他平台线程通过工作窃取算法窃取虚拟线程任务并执行,这样我们就可以做到利用有限的线程资源去开启大量的虚拟线程,从而提高CPU的利用率。
# 使用
虚拟线程的创建方式,主要有以下 4 种:
- Thread.startVirtualThread(Runnable task)
- Thread.ofVirtual().unstarted(Runnable task)
- Thread.ofVirtual().factory()
- Executors.newVirtualThreadPerTaskExecutor()
示例
public static void main(String[] args) throws InterruptedException {
// 只创建虚拟线程,但不直接启动
Thread vt1 = Thread.ofVirtual().unstarted(() -> {
String vtName = Thread.currentThread().toString();
System.out.println(vtName + " 线程执行");
});
// 启动
vt1.start();
// 创建并启动虚拟线程
Thread.startVirtualThread(() -> {
String vtName = Thread.currentThread().toString();
System.out.println(vtName + " 线程执行");
});
// 先创建虚拟线程工厂,然后再使用工厂创建虚拟线程,之后再调用 start() 方法进行执行
ThreadFactory tf = Thread.ofVirtual().factory();
Thread vt2 = tf.newThread(()->{
String vtName = Thread.currentThread().toString();
System.out.println(vtName + " 线程执行");
});
vt2.start();
// 使用线程池的方式创建虚拟线程
try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
// 提交任务
executor.submit(() -> {
String vtName = Thread.currentThread().toString();
System.out.println(vtName + " 线程执行");
});
}
// 阻塞主线程
Thread.currentThread().join();
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
打印
VirtualThread[#34]/runnable@ForkJoinPool-1-worker-2 线程执行
VirtualThread[#32]/runnable@ForkJoinPool-1-worker-1 线程执行
VirtualThread[#36]/runnable@ForkJoinPool-1-worker-3 线程执行
VirtualThread[#38]/runnable@ForkJoinPool-1-worker-2 线程执行
2
3
4
# 测试
# 运行创建100w个虚拟线程
private static void test1() {
CopyOnWriteArraySet<Object> list = new CopyOnWriteArraySet<>();
// 创建100w个虚拟线程
try (ExecutorService executorService = Executors.newVirtualThreadPerTaskExecutor()) {
IntStream.range(1, 1_000_000).forEach(value -> {
executorService.submit(() -> {
String threadInfo = Thread.currentThread().toString();
list.add(threadInfo.split("@")[1]);
});
});
}
System.out.println(list);
}
2
3
4
5
6
7
8
9
10
11
12
13
打印
[ForkJoinPool-1-worker-1,
ForkJoinPool-1-worker-2,
ForkJoinPool-1-worker-8,
ForkJoinPool-1-worker-6,
ForkJoinPool-1-worker-5,
ForkJoinPool-1-worker-7,
ForkJoinPool-1-worker-3,
ForkJoinPool-1-worker-4,
ForkJoinPool-1-worker-10,
ForkJoinPool-1-worker-14,
ForkJoinPool-1-worker-13,
ForkJoinPool-1-worker-12,
ForkJoinPool-1-worker-11,
ForkJoinPool-1-worker-9,
ForkJoinPool-1-worker-16,
ForkJoinPool-1-worker-15]
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
有16个worker,相当于是16个平台线程,刚好对应我的16核CPU,也就是全部利用到了
# IO密集场景测试
分别用虚拟线程和平台线程池测试IO阻塞场景的耗时
/**
* 测试虚拟线程
*/
private static void test2() {
long start = System.currentTimeMillis();
// 模拟完成一段耗时的IO任务
try (ExecutorService executorService = Executors.newVirtualThreadPerTaskExecutor()) {
IntStream.range(1, 1_000).forEach(value -> {
executorService.submit(() -> {
try {
// 模拟io阻塞
Thread.sleep(1000);
} catch (InterruptedException ignored) {}});
});
}
System.out.println("虚拟线程耗时:" + (System.currentTimeMillis() - start) + "ms");
}
/**
* 测试平台线程池
*/
private static void test3() {
long start = System.currentTimeMillis();
// 模拟完成一段耗时的IO任务
try (ExecutorService executorService =
new ThreadPoolExecutor(10,
16,
10,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(1_000))) {
IntStream.range(1, 1_000).forEach(value -> {
executorService.submit(() -> {
try {
// 模拟io阻塞
Thread.sleep(1000);
} catch (InterruptedException ignored) {}});
});
}
System.out.println("平台线程池耗时:" + (System.currentTimeMillis() - start) + "ms");
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
打印
虚拟线程耗时:1069ms
平台线程池耗时:101081ms
2
虽然线程池参数不是最优,但结果的差距还是很明显的,不过要注意的是,如果执行的是一段CPU密集型操作,而不是IO密集操作,那么两者的性能差距会极大程度缩短,因为虚拟线程的本质是调用被空闲的CPU资源,如果CPU资源被耗尽的极端情况,一样会出现阻塞。我们可以再做个测试:
# CPU密集操作场景测试
/**
* 测试虚拟线程
*/
private static void test4() {
long start = System.currentTimeMillis();
// 模拟完成一段耗时的CPU任务
try (ExecutorService executorService = Executors.newVirtualThreadPerTaskExecutor()) {
IntStream.range(1, 50).forEach(value -> {
executorService.submit(VirtualThreadDemo::cpuTask);
});
}
System.out.println("虚拟线程耗时:" + (System.currentTimeMillis() - start) + "ms");
}
/**
* 测试平台线程池
*/
private static void test5() {
long start = System.currentTimeMillis();
// 模拟完成一段耗时的CPU任务
try (ExecutorService executorService =
new ThreadPoolExecutor(10,
16,
10,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(1_000))) {
IntStream.range(1, 50).forEach(value -> {
executorService.submit(VirtualThreadDemo::cpuTask);
});
}
System.out.println("平台线程池耗时:" + (System.currentTimeMillis() - start));
}
/**
* 模拟执行5s的CPU任务
*/
private static void cpuTask() {
long startTime = System.currentTimeMillis();
// 执行5s
while (System.currentTimeMillis() < startTime + 5_000) {
// 消耗CPU资源
int sum = 0;
for (int i = 0; i < 10000000; i++) {
sum += i;
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
打印
虚拟线程耗时:20037ms
平台线程池耗时:25029ms
2
而且我们再观察一下CPU的利用率:
虚拟线程
平台线程池
可以看到即便是CPU密集的操作,平台线程池还是没法充分利用CPU资源