第十六章. common-core 前沿:虚拟线程(Virtual Threads)适配与实战

第十六章. common-core 前沿:RVP 5.x 虚拟线程实战

摘要:本章我们将深入探索 RVP 5.x 对 Java 21 虚拟线程的深度适配,聚焦于框架层面的配置、源码实现与性能实战。通过操纵 RVP 的“一键切换”机制,我们将亲手验证虚拟线程在高并发 IO 场景下惊人的吞吐量优势。

在上一章中,我们已经掌握了 @Async 异步调用的配置方法与常见的陷阱。这为我们处理耗时任务提供了有力的武器。然而,随着 Java 21 的正式发布,一项名为 虚拟线程 (Virtual Threads) 的革命性特性,为高并发编程带来了全新的解决方案。

RVP 5.x 作为一个紧跟技术前沿的框架,内置了对虚拟线程的完整支持。在本章,我们不再从零开始解释虚拟线程的底层原理,而是将目光聚焦于框架本身,通过分析源码与实际动手测试,去回答两个核心问题:

  1. RVP 是如何巧妙地实现平台线程与虚拟线程的“一键切换”的?
  2. 这种切换在真实的业务场景中,能带来多大的性能提升?

16.1. RVP 5.x 虚拟线程的“总开关”

RVP 5.x 构建于 Spring Boot 3.2+ 之上,这意味着它天然享受了 Spring 生态对虚拟线程的强大支持。 令人惊喜的是,启用这一强大的特性,我们仅仅需要修改一行配置。

16.1.1. 环境检查:JDK 21

虚拟线程是 Java 21 提供的正式功能,在此之前的版本无法使用。在开始之前,请务必确认我们的开发环境已经准备就绪。

  • JDK 版本:必须安装 JDK 21 或更高版本。
  • Maven 配置:检查项目根目录下的 pom.xml 文件,确保 Java 版本已正确指定。
1
2
3
4
<!-- 文件路径: pom.xml -->
<properties>
<java.version>21</java.version>
</properties>
  • IDE 配置:以 IntelliJ IDEA 为例,需要确保项目的 SDK 和语言级别均设置为 21。
    • File -> Project Structure -> Project
    • SDK: 选择 21
    • Language Level: 选择 21 - Switch expressions, records, etc.

16.1.2. 核心配置:application.yml

默认情况下,RVP 为了保持最大的兼容性,并未开启虚拟线程。我们需要手动激活它。

我们来打开 ruoyi-admin 模块的配置文件,找到并修改虚拟线程的开关。

文件路径ruoyi-admin/src/main/resources/application.yml

1
2
3
4
5
6
7
spring:
threads:
virtual:
# 【核心开关】开启或关闭虚拟线程 (仅在 JDK 21+ 环境下生效)
# true: 代表开启,应用将尽可能使用虚拟线程处理并发任务
# false: 代表关闭 (这是默认值),应用将继续使用传统的平台线程池
enabled: true

16.1.3. 自动装配:Spring Boot 3+ 的自动化切换

当我们将 enabled 开关设为 true 并重启应用后,Spring Boot 的自动配置机制(具体由 VirtualThreadsAutoConfiguration 类实现)会立刻生效,并对整个应用的并发模型产生两个至关重要的、全局性的影响。

  1. Web 服务器线程模型切换

    • 无论是 Tomcat 还是 Undertow,Spring Boot 会自动将其处理 HTTP 请求的工作线程池,替换为一个“虚拟线程执行器”。
    • 这意味着,今后每一个进入我们系统的 HTTP 请求,都会由一个全新的、轻量级的虚拟线程来处理。我们再也无需担心传统线程池因数量有限而成为并发瓶颈的问题。
  2. @Async 异步任务线程池切换

    • Spring Boot 会将我们所熟知的、用于执行 @Async 任务的默认线程池(通常以 async- 作为前缀)也一并切换为虚拟线程模式。
    • 这带来的直接好处是,所有通过 @Async 注解提交的异步任务,都将自动地在虚拟线程中运行,实现了对业务代码的完全透明。

16.1.4. 本节小结

核心要点

  • 环境先行:虚拟线程强依赖于 JDK 21 环境。
  • 一键开启:通过 spring.threads.virtual.enabled: true 即可全局激活。
  • 自动适配:Spring Boot 3.2+ 会自动将 Web 服务器和 @Async 任务切换至虚拟线程模式。
1
2
3
4
5
# 在 application.yml 中开启虚拟线程
spring:
threads:
virtual:
enabled: true

16.2. 源码解析:RVP 线程池的自动适配

在上一节中,我们已经掌握了如何通过 application.yml 的总开关来激活虚拟线程,并理解了这主要得益于 Spring Boot 的自动配置。但在实际开发中,我们还会遇到 RVP 框架自身定义的线程池,比如 ThreadPoolConfig 中配置的定时任务线程池。本节我们将深入 RVP 的源码,探索它是如何对虚拟线程进行深度适配,以确保框架的每一个角落都能享受到新技术带来的红利。

16.2.1. 源码追踪:SpringUtils.isVirtual()

为了让框架的各个模块能够方便地感知“虚拟线程开关”的状态,RVP 在其核心工具类 SpringUtils 中封装了一个静态判断方法。

文件路径ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/SpringUtils.java

1
2
3
4
5
6
7
8
9
/**
* 判断当前是否已开启虚拟线程
* @return 如果 spring.threads.virtual.enabled 配置为 true,则返回 true,否则返回 false
*/
public static boolean isVirtual() {
// Spring Boot 内部通过 Threading 枚举来管理线程模式
// 这行代码的本质就是检查环境变量或配置文件中 spring.threads.virtual.enabled 的值
return Threading.VIRTUAL.isActive(getBean(Environment.class));
}

这个小小的工具方法是 RVP 实现动态切换的关键。它为框架内所有需要根据线程模式做出不同行为的组件,提供了一个统一的、可靠的判断依据。

16.2.2. 源码追踪:ThreadPoolConfig 动态切换

现在,我们带着 isVirtual() 这个线索,回到我们在第十四章分析过的 ThreadPoolConfig,看看它内部隐藏的适配逻辑。

文件路径ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/config/ThreadPoolConfig.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Bean(name = "scheduledExecutorService")
protected ScheduledExecutorService scheduledExecutorService() {
// 创建一个基础的线程工厂构造器,并设置为守护线程
BasicThreadFactory.Builder builder = new BasicThreadFactory.Builder().daemon(true);

// 【核心适配逻辑开始】
if (SpringUtils.isVirtual()) {
// 场景一:如果判断出已开启虚拟线程
// 1. 设置线程名称格式,方便日志排查
// 2. 将底层的线程工厂替换为 Spring 的虚拟线程工厂包装器
builder.namingPattern("virtual-schedule-pool-%d")
.wrappedFactory(new VirtualThreadTaskExecutor().getVirtualThreadFactory());
} else {
// 场景二:如果未开启虚拟线程 (即传统平台线程模式)
// 仅设置传统的线程命名格式
builder.namingPattern("schedule-pool-%d");
}
// 【核心适配逻辑结束】

// 使用上面动态配置好的 builder 来构建最终的 ScheduledThreadPoolExecutor
return new ScheduledThreadPoolExecutor(core, builder.build(), ...);
}

代码逻辑剖析

这段代码的设计非常精妙,它为我们揭示了 RVP 的适配思路:

  • 未开启虚拟线程时builder 使用默认的线程工厂,创建出的都是普通的平台线程。通过日志或监控,我们会看到线程名是 schedule-pool-1, schedule-pool-2
  • 开启虚拟线程时:RVP 并没有试图去改变 ScheduledThreadPoolExecutorcorePoolSize(核心池大小)等参数,因为这些参数对于几乎无限的虚拟线程来说意义不大。相反,它通过 .wrappedFactory() 方法,釜底抽薪,直接替换了最底层的 线程工厂 (ThreadFactory)

这带来的结果是,ScheduledThreadPoolExecutor 的结构和行为还在,但它每次向工厂请求一个“新线程”时,得到的不再是重量级的平台线程,而是一个轻量级的 虚拟线程。最终,我们会在日志中看到名为 virtual-schedule-pool-1… 的线程在执行任务。


16.2.3. 本节小结

核心要点

  • 统一判断入口:RVP 通过 SpringUtils.isVirtual() 方法,提供了一个全局的虚拟线程状态检测点。
  • 适配核心思想:RVP 的适配策略不是修改线程池参数,而是通过 if-else 判断,在运行时动态替换底层的 ThreadFactory
  • 对业务透明:这种底层的动态切换,对上层业务代码(例如注入并使用 scheduledExecutorService 的地方)是完全无感的,实现了技术的平滑升级。

16.3. 性能压测:平台线程 vs 虚拟线程

在前面的章节,我们已经从配置和源码层面理解了 RVP 的虚拟线程机制。但理论终究是理论,它在实战中的效果究竟如何?本节,我们将化身测试工程师,通过构建一个高并发模拟场景,用真实的数据来直观对比两种线程模型在性能上的巨大差异。

16.3.1. 测试准备:创建 VirtualController

我们的目标是模拟一个典型的 IO 密集型任务。在真实业务中,这通常对应着数据库查询、调用第三方 API、读写文件等操作。这些操作的共同特点是,线程在大部分时间内都处于等待状态(WAITINGBLOCKED),而不是在消耗 CPU。

我们将使用 Thread.sleep() 来精准地模拟这种耗时等待。

第一步:规划文件位置

我们在 ruoyi-demo 模块下创建用于测试的 Controller。

1
2
3
ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/
└── controller/
└── VirtualController.java # 1. 在这里创建我们的测试控制器

第二步:编写控制器骨架

我们先定义好类的基本结构,并注入 ScheduledExecutorService。请注意,我们注入的正是上一节分析过的、会根据配置动态切换线程模式的那个 Bean。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package org.dromara.demo.controller;

import cn.dev33.satoken.annotation.SaIgnore;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@SaIgnore
@Slf4j
@RequiredArgsConstructor
@RestController
@RequestMapping("/virtual")
public class VirtualController {

}

第三步:实现 IO 模拟与压测接口

接下来,我们添加两个核心方法:一个用于模拟耗时 100 毫秒的 IO 操作,另一个作为我们的压测入口,它会并发提交 1000 个这样的 IO 任务。

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
37
38
39
40
41
42
43
44
45
46
47
48
49
// ... 接上文 ...
public class VirtualController {

private final ScheduledExecutorService scheduledExecutorService;

/**
* 模拟一个 IO 密集型任务
* 通过休眠 100 毫秒来代表一次数据库查询或远程服务调用
*/
private void ioTask() {
try {
// 使用 Hutool 工具类,让代码更简洁
ThreadUtil.sleep(100, TimeUnit.MILLISECONDS);
} catch (Exception e) {
// 在实际测试中,我们忽略中断异常
}
}

/**
* 压测接口:并发提交 1000 个模拟的 IO 任务
*/
@GetMapping("/bench")
public R<String> benchmark() throws Exception {
int taskCount = 1000; // 定义任务总数
log.info("开始压测,任务总数: {}", taskCount);
long start = System.currentTimeMillis();

List<Future<?>> futures = new ArrayList<>(taskCount);

// 1. 批量向线程池提交 1000 个任务
for (int i = 0; i < taskCount; i++) {
futures.add(scheduledExecutorService.submit(this::ioTask));
}

// 2. 等待所有任务执行完成
// future.get() 是一个阻塞操作,会一直等到该任务执行完毕
for (Future<?> future : futures) {
future.get();
}

long end = System.currentTimeMillis();
long cost = end - start;

// 打印一条日志,用于观察当前执行压测任务的线程是什么类型
log.info("压测结束。当前线程示例: {}", Thread.currentThread().toString());

return R.ok("任务数: " + taskCount + ", 总耗时: " + cost + " ms");
}
}

16.3.2. 场景一:关闭虚拟线程 (模拟传统模式)

首先,我们来模拟传统的、基于平台线程池的并发模型。

  1. 修改配置:在 ruoyi-admin/src/main/resources/application.yml 中,将虚拟线程开关关闭。

    1
    2
    3
    4
    spring:
    threads:
    virtual:
    enabled: false
  2. 重启应用:确保配置生效。

  3. 调用接口:使用浏览器或 API 工具访问 GET http://localhost:8080/virtual/bench

结果分析

  • 日志观察:在控制台日志中,我们可以看到执行任务的线程名类似于 schedule-pool-1,这是典型的平台线程名称。
  • 耗时分析:假设我们的 ThreadPoolConfig 中配置的核心线程数 corePoolSize 为默认的 33(根据 32 核服务器优化)。这意味着,线程池一次最多只能同时处理 33 个任务。
    • 1000 个任务,每个耗时 100ms,并发度为 33。
    • 理论总耗时 ≈ (1000 个任务 / 33 并发度) * 100 毫秒30.3 * 1003030 毫秒 (约 3 秒)
  • 核心瓶颈:我们可以清晰地看到,性能瓶颈在于 平台线程的数量。绝大多数任务(1000 - 33 = 967 个)在提交后,并没有立刻执行,而是在任务队列中排队,等待有空闲的线程来处理它们。

16.3.3. 场景二:开启虚拟线程 (RVP 5.x 模式)

现在,让我们揭晓谜底,看看 RVP 5.x 在开启虚拟线程后的表现。

  1. 修改配置:在 application.yml 中,重新开启虚拟线程。
    1
    2
    3
    4
    spring:
    threads:
    virtual:
    enabled: true
  2. 重启应用
  3. 调用接口:再次访问 GET http://localhost:8080/virtual/bench

结果分析

  • 日志观察:此时,日志中的线程名会变为 VirtualThread[#...]virtual-schedule-pool-1 这样的形式,明确告诉我们任务正由虚拟线程执行。
  • 耗时分析:虚拟线程的创建成本极低,JVM 可以轻松创建成千上万个。当我们提交 1000 个任务时,线程池(现在由虚拟线程工厂驱动)几乎可以瞬间创建 1000 个虚拟线程,让它们 同时开始执行
    • 1000 个任务几乎是同时并发,每个任务耗时 100ms。
    • 理论总耗时 ≈ 100 毫秒(加上一些非常小的调度开销)。
    • 实际测试结果通常在 100ms - 150ms 之间。
  • 性能飞跃:对比场景一的 3000ms,性能提升了近 30 倍! 所有的任务几乎是瞬间完成,没有任何排队等待。虚拟线程彻底消除了应用层因线程数量不足而导致的等待瓶颈。

注意:在真实的复杂业务中,开启虚拟线程后,性能瓶颈可能会转移到其他地方,例如数据库连接池大小、网络带宽或第三方服务的 QPS 限制。但它无疑解决了应用本身因线程阻塞造成的吞吐量问题。


16.3.4. 本节小结

核心要点

  • IO 密集型任务Thread.sleep() 是模拟数据库查询、API 调用等 IO 等待的有效手段。
  • 平台线程瓶颈:传统模式下,性能受限于 corePoolSize,大量任务需要排队。
  • 虚拟线程优势:虚拟线程几乎没有数量限制,能将“排队执行”变为“并发执行”,特别适合 IO 密集型场景,可带来数量级的性能提升。

16.4. 本章总结

在本章的学习中,我们共同见证了 RVP 5.x 是如何紧跟 Java 技术前沿,将 Java 21 的虚拟线程特性无缝融入框架之中的。

我们从一个简单的配置开关 spring.threads.virtual.enabled 入手,理解了 RVP 开启虚拟线程的便捷性。这不仅仅是一个表面的配置,背后是 Spring Boot 3.2+ 强大的自动装配能力,它能够自动将 Web 容器(如 Tomcat)和 @Async 异步任务的执行引擎切换到虚拟线程模式。

随后,我们通过深入 ThreadPoolConfig 的源码,揭示了 RVP 自身的适配智慧。它并没有粗暴地修改线程池参数,而是巧妙地通过判断 SpringUtils.isVirtual() 的状态,在运行时动态地替换底层的 ThreadFactory。这种“釜底抽薪”式的设计,在不改变上层代码任何调用的前提下,完成了对自定义线程池的虚拟化改造,充分体现了框架设计的优雅与前瞻性。

最后,也是最激动人心的部分,我们通过亲手编写压测代码,直观地感受到了虚拟线程带来的巨大性能飞跃。在模拟高并发 IO 密集型任务的场景下,从传统平台线程切换到虚拟线程,处理 1000 个并发任务的耗时从 3 秒急剧缩短至约 100 毫秒,性能提升了近 30 倍。 这个实验无可辩驳地证明了虚拟线程在消除线程等待瓶颈方面的强大威力。

【二次开发建议】

  • 拥抱新技术:如果你的 RVP 项目正在或计划在 JDK 21 环境下运行,并且业务场景中包含大量的数据库访问、微服务调用等 IO 操作(这几乎是所有 CRUD 业务系统的常态),那么我们 强烈建议你开启此开关
  • 免费的性能红利:这几乎是一次零成本的性能优化,只需一行配置,就能让你的应用在高并发下表现得更加从容。
  • 唯一的注意事项:在开启前,请审视你的代码(以及所依赖的第三方库)中,是否存在长时间持有 synchronized 锁并进行 IO 操作的逻辑。这是因为 synchronized 关键字可能会导致平台线程被“钉住” (pinning),从而使虚拟线程无法发挥其优势。这是目前使用虚拟线程时需要注意的少数情况之一。