认识虚拟线程

在Java21中正式引入了虚拟线程,它与我们目前所使用的传统线程(也叫平台线程)有什么区别,本文针对虚拟线程来进行研究并实战一些案例,帮助大家理解。

虚拟线程是在Java19中初次出现并在Java21中正式推出。与平台线程相比,它具有以下优势:

轻量化:虚拟线程是一个轻量化的线程,由JVM管理,所以创建和销毁的开销都很小,可以轻松创建上百万个线程。平台线程则是由系统内核直接管理,一个平台线程对应一个系统内核,自然创建和销毁的开销成本会比较高

无阻塞影响:在平台线程中,如果遇到阻塞时会阻塞操作系统对应的线程,导致降低系统的并发的能力。在虚拟线程中,如果遇到阻塞时并不会真正阻塞操作系统的线程,而是会自动挂起并释放资源,保证系统线程的可用性

资源管理:因为虚拟线程轻量化的原因,所以永远不应该池化,每个任务都会创建一个新的虚拟线程,使用完成后销毁。而平台线程则因为重量级,所以必须进行池化来进行资源复用

调度效率:因为虚拟线程是JVM管理的,所以可以快速切换上下文,而平台线程是由系统内核管理,需要更多的上下文切换时间

综上所述,虚拟线程十分适合处理高并发、I/O密集查询等场景,下面开始虚拟线程的使用。

必要环境

Java21(虚拟线程在Java21正式登场):本文使用Java21

SpringBoot版本:3.2.0(SpringBoot在3.2.0以后的版本支持虚拟线程):本文使用版本为3.3.2

Lombok:1.18.30及以上(lombok也需要进行版本升级):本文使用版本为1.8.34

项目配置

在SpringBoot3.2.0之后我们只需要在配置文件中开启虚拟线程即可

1
2
3
4
spring:
threads:
virtual:
enabled: true

创建虚拟线程的方法

  1. Thread.ofVirtual().start(Runnable):通过start方法来启动虚拟线程,也可以使用unstart方法手动启动
  2. Thread.startVirtualThread(Runnable):通过startVirtualThread方法启动虚拟线程,参数为继承了Runnable的类
  3. Executors.newVirtualThreadPerTaskExecutor().submit(Runnable):通过虚拟线程执行器创建并启动虚拟线程
  4. Thread.ofVirtual().factory().newThread(Runnable).start():通过虚拟线程工厂创建并启动
1
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
public static class MyThread implements Runnable{
@Override
public void run() {
System.out.println("2.通过startVirtualThread方法创建虚拟线程");
}
}

/**
* 创建虚拟线程的四种方法
* @return
*/
@GetMapping("/thread00")
public Result thread00() {
// 1 直接创建并启动
Runnable firstAuto = () -> System.out.println("1.创建并启动虚拟线程");
Thread.ofVirtual().start(firstAuto);
// 1 直接创建手动启动
Runnable first = () -> System.out.println("1.创建不启动虚拟线程");
Thread unstarted = Thread.ofVirtual().unstarted(first);
unstarted.start();

// 2 通过startVirtualThread方法创建并启动
Thread.startVirtualThread(new MyThread());

// 3 通过虚拟线程执行器创建
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
executor.submit(() -> System.out.println("3.虚拟线程执行器启动虚拟线程"));
}

// 4 通过虚拟线程工厂创建
ThreadFactory factory = Thread.ofVirtual().factory();
Thread factoryThread = factory.newThread(() -> System.out.println("4.虚拟线程工厂启动虚拟线程"));
factoryThread.start();

return Result.success();
}

高并发场景测试

测试的场景为:读取本地的一个html文件,并将其复制到另一个地方,完成后继续读取数据库的一张表。共循环10000次,执行3次,观察虚拟线程和平台线程的性能。 同时为了保证正确的循环次数,防止部分线程未执行操作就被抛弃等误差,使用AtomicInteger原子类对完成任务的线程进行数量累加,只有正确完成10000次的结果才会被算入。

虚拟线程:

1
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
public Result virtualThread() {
StopWatch sw = new StopWatch();
AtomicInteger atomicInteger = new AtomicInteger();
sw.start();
try (ExecutorService executorService = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 10000; i++) {
executorService.submit(() -> {
try {
// 读取文件
File file = new File("/Users/sora33/Pictures/n06.html");
if (file.canRead()) {
InputStreamReader inputStreamReader = new InputStreamReader(new FileInputStream(file), "utf-8");
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
bufferedReader.lines().forEach(System.out::println);
// 拷贝到本地路径
FileCopyUtils.copy(file, new File("/Users/sora33/Pictures/test.html"));
}
List<User> users = userMapper.selectAll();
atomicInteger.addAndGet(1);
} catch (Exception e) {
throw new RuntimeException(e);
}
});
}
}
// 开启一个循环,只有当循环逻辑全部执行完成才计算结束时间
while (true) {
int i = atomicInteger.get();
if (i == 10000) {
sw.stop();
System.out.println("虚拟线程访问时间:" + sw.getLastTaskTimeMillis() + ",循环次数:" + i);
break;
}
}
return Result.success();
}

平台线程:

1
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
public Result platformThread() {
StopWatch sw = new StopWatch();
AtomicInteger atomicInteger = new AtomicInteger();
sw.start();
for (int i = 0; i < 10000; i++) {
Thread thread = new Thread(() -> {
try {
File file = new File("/Users/sora33/Pictures/n06.html");
if (file.canRead()) {
InputStreamReader inputStreamReader = new InputStreamReader(new FileInputStream(file), "utf-8");
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
bufferedReader.lines().forEach(System.out::println);
FileCopyUtils.copy(file, new File("/Users/sora33/Pictures/test.html"));
}
List<User> users = userMapper.selectAll();
atomicInteger.addAndGet(1);
} catch (Exception e) {
throw new RuntimeException(e);
}
});
thread.start();
}
while (true) {
int i = atomicInteger.get();
if (i == 10000) {
sw.stop();
System.out.println("平台线程访问时间:" + sw.getLastTaskTimeMillis() + ",循环次数:" + i);
break;
}
}
return Result.success();
}

平台线程结果:

image-20240815160239303 image-20240815160254206 image-20240815160302258 image-20240815160317289

虚拟线程结果:

image-20240815160434734 image-20240815160445610 image-20240815160459259 image-20240815160511926

结论汇总:

可以从两边结果看出来,平台线程的结果不是很稳定,最慢的一次是4s,最快则为3.2s,而虚拟线程的结果要稳定的多,同时速度也要快一点。后面我又用线程池跟虚拟线程比了一下,线程池总体而言要比虚拟线程快一点,但线程池对资源,以及使用上都要比虚拟线程麻烦,所以目前来看,在适合的场景下,可以优先考虑使用虚拟线程,不过线程池也完全可以帮助我们解决一些业务上的问题(但业务内如果涉及到阻塞或者逻辑运行时间过长,虚拟线程则会有更大的优势,因为可以不断创建虚拟线程来完成任务,而线程池则必须等到当前线程执行完逻辑后才会继续从队列中获取下一个任务)

结束语

通过对比虚拟线程和平台线程的性能差异,可以看到虚拟线程在适合的场景下发挥出更好的性能,如高并发和I/O密集的操作中。同时其轻量化和无阻塞的特性未来也十分具有竞争力。以上就是对虚拟线程的一些认识与实战,如果本文对你有帮助,欢迎分享给其他人。