Java 21 虚拟线程与 Spring Boot 3 实战教程:从原理到生产落地
Java 21 虚拟线程与 Spring Boot 3 实战教程
适用读者:熟悉 Java 基础与 Spring Boot,希望在生产环境落地虚拟线程的后端工程师。
预计学习时长:45 分钟 · 含动手练习
学习目标
完成本教程后,你将能够:
- 解释虚拟线程与平台线程(Platform Thread)的本质区别及适用场景
- 在 Spring Boot 3.2+ 项目中一行配置启用虚拟线程
- 识别虚拟线程的「最佳实践」与「反模式」,避免 pin 住载体线程
- 使用 JFR、Micrometer 监控虚拟线程的运行状态
- 将现有基于线程池的阻塞 I/O 服务迁移到虚拟线程架构
前置知识
- Java 17+ 语法基础(record、sealed class 等了解即可)
- Spring Boot 3.x 项目结构与依赖管理(Maven/Gradle)
- 理解阻塞 I/O 与线程模型的基本关系
- 了解
ExecutorService、ThreadPoolExecutor的基本用法
第一章:虚拟线程是什么?
1.1 传统线程模型的瓶颈
在 Java 传统模型中,每个 Thread 对应一个操作系统线程(平台线程)。OS 线程创建成本高(约 1MB 栈空间),且上下文切换开销大。因此高并发 Web 服务通常使用「少量平台线程 + 线程池」处理请求。
当业务以 阻塞 I/O 为主(数据库查询、HTTP 调用、文件读写),线程在等待 I/O 返回期间无法处理其他任务,造成线程池耗尽、请求排队。
1.2 虚拟线程的核心思想
Java 21 正式引入的 虚拟线程(Virtual Threads) 由 JVM 调度,挂载在少量平台线程(载体线程 Carrier Thread)上运行。当虚拟线程执行阻塞操作时,JVM 自动将其「卸载」(unmount),让载体线程去执行其他虚拟线程,从而实现 用同步代码风格写出异步级别的并发能力。
关键特性:
| 特性 | 平台线程 | 虚拟线程 |
|---|---|---|
| 创建成本 | 高(~1MB 栈) | 极低(~几 KB) |
| 数量上限 | 数千 | 数百万 |
| 调度方 | OS | JVM |
| 适用场景 | CPU 密集 | I/O 密集 |
1.3 验证环境
确认 JDK 版本:
java -version
# 应显示 openjdk version "21" 或更高
创建测试项目:
curl https://start.spring.io/starter.zip \
-d dependencies=web,actuator \
-d javaVersion=21 \
-d bootVersion=3.3.0 \
-o vt-demo.zip && unzip vt-demo.zip -d vt-demo
第二章:Spring Boot 启用虚拟线程
2.1 一行配置启用
Spring Boot 3.2 起内置虚拟线程支持。在 application.yml 中添加:
spring:
threads:
virtual:
enabled: true
此配置会让 Spring MVC 的请求处理、@Async 任务、TaskExecutor 等默认使用虚拟线程。
2.2 验证是否生效
编写测试 Controller:
@RestController
@RequestMapping("/api/demo")
public class ThreadInfoController {
@GetMapping("/thread")
public Map<String, Object> threadInfo() {
Thread t = Thread.currentThread();
return Map.of(
"name", t.getName(),
"virtual", t.isVirtual(),
"threadId", t.threadId()
);
}
}
启动后访问 http://localhost:8080/api/demo/thread,应看到:
{
"name": "",
"virtual": true,
"threadId": 42
}
virtual: true 表示请求由虚拟线程处理。
2.3 手动创建虚拟线程
不依赖 Spring 时,可用 JDK API:
// 方式一:Thread.ofVirtual()
Thread vThread = Thread.ofVirtual()
.name("worker-", 0)
.start(() -> System.out.println("Hello from virtual thread"));
// 方式二:Executors.newVirtualThreadPerTaskExecutor()
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
IntStream.range(0, 100_000).forEach(i ->
executor.submit(() -> {
Thread.sleep(Duration.ofSeconds(1));
return i;
})
);
} // 自动关闭,等待所有任务完成
第三章:实战——高并发 HTTP 客户端
3.1 场景描述
某订单服务需要并发调用 3 个下游微服务(库存、优惠、物流),每个调用耗时 200ms。使用平台线程池时,1000 并发需要大量线程;虚拟线程可轻松处理。
3.2 实现代码
@Service
public class OrderAggregationService {
private final RestClient restClient;
public OrderAggregationService(RestClient.Builder builder) {
this.restClient = builder.baseUrl("http://downstream").build();
}
public OrderDetail aggregate(String orderId) {
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
Future<StockInfo> stockFuture = executor.submit(() ->
restClient.get().uri("/stock/{id}", orderId)
.retrieve().body(StockInfo.class));
Future<DiscountInfo> discountFuture = executor.submit(() ->
restClient.get().uri("/discount/{id}", orderId)
.retrieve().body(DiscountInfo.class));
Future<LogisticsInfo> logisticsFuture = executor.submit(() ->
restClient.get().uri("/logistics/{id}", orderId)
.retrieve().body(LogisticsInfo.class));
return new OrderDetail(
stockFuture.get(),
discountFuture.get(),
logisticsFuture.get()
);
} catch (Exception e) {
throw new ServiceException("聚合失败", e);
}
}
}
三个下游调用并行执行,总耗时约等于最慢的一个(~200ms),而非串行的 600ms。
3.3 性能对比实验
使用 wrk 压测:
# 虚拟线程开启
wrk -t4 -c500 -d30s http://localhost:8080/api/orders/12345
# 关闭虚拟线程后对比
# spring.threads.virtual.enabled=false
在 I/O 密集场景下,虚拟线程通常能以更少内存支撑更高并发,且 P99 延迟更稳定。
第四章:陷阱与最佳实践
4.1 避免 synchronized 导致 pin
虚拟线程在 synchronized 块内阻塞时,无法卸载,会 pin 住载体线程,退化为平台线程行为。JDK 21 中建议使用 ReentrantLock 替代:
// 反模式:pin 住载体线程
synchronized (lock) {
Thread.sleep(1000); // 阻塞期间载体线程被占用
}
// 推荐:使用 ReentrantLock
lock.lock();
try {
Thread.sleep(1000); // 虚拟线程可正常卸载
} finally {
lock.unlock();
}
4.2 线程本地变量(ThreadLocal)
虚拟线程数量巨大,滥用 ThreadLocal 可能导致内存泄漏。优先使用 ScopedValue(Java 21 预览)或显式传参。
4.3 不适合虚拟线程的场景
- CPU 密集计算:图像处理、加密运算、大数据聚合——应使用平台线程池 + 固定大小
- Native 代码长时间阻塞:JNI 调用不会触发卸载
- 自定义线程池混用:确保池内全部是虚拟线程或全部是平台线程
4.4 监控指标
启用 Actuator + Micrometer:
management:
endpoints:
web:
exposure:
include: metrics,threaddump
关注指标:
jvm.threads.virtual:当前虚拟线程数jvm.threads.live:存活线程总数- 通过 JFR 事件
jdk.VirtualThreadPinned检测 pin 问题
第五章:从线程池迁移的决策清单
迁移前评估:
- 服务是否以阻塞 I/O 为主?(是 → 适合)
- 是否大量使用
synchronized?(是 → 先重构为 Lock) - 是否依赖固定大小线程池做限流?(是 → 保留平台线程池做限流,业务逻辑用虚拟线程)
- 下游连接池大小是否匹配?(虚拟线程并发高时,需调大 HikariCP、HttpClient 连接池)
# HikariCP 示例:虚拟线程下可适当增大
spring:
datasource:
hikari:
maximum-pool-size: 50
练习 / 作业
- 基础练习:创建 Spring Boot 项目,启用虚拟线程,编写接口返回当前线程信息,截图验证
isVirtual() == true。 - 并发练习:实现一个接口,并发请求 5 个公开 HTTP API(如 GitHub API),聚合结果返回。对比虚拟线程开启前后的压测数据。
- 排障练习:故意在
synchronized块内Thread.sleep(5000),用 JFR 录制并查找VirtualThreadPinned事件。 - 进阶作业:将团队内一个使用
@Async+ 固定线程池的服务迁移到虚拟线程,记录内存与延迟变化,撰写迁移报告。
FAQ
Q:虚拟线程能替代响应式编程(WebFlux)吗?
A:在多数 I/O 密集场景可以。虚拟线程让你用同步代码获得类似吞吐,且调试更简单。但若团队已深度投入 Reactor 生态,迁移成本需评估。
Q:Spring Boot 2.x 能用虚拟线程吗?
A:需要 Spring Boot 3.2+。2.x 可手动使用 Executors.newVirtualThreadPerTaskExecutor(),但无法一行启用 MVC 虚拟线程。
Q:虚拟线程与 Goroutine 一样吗?
A:理念相似,但实现不同。Go 的 goroutine 是语言级调度;Java 虚拟线程是 JVM 级,与现有 Java 生态无缝兼容。
Q:生产环境 JDK 21 稳定吗?
A:JDK 21 是 LTS 版本,虚拟线程已正式发布(非预览)。Spring Boot 3.2+ 官方支持,国内已有大量生产案例。
Q:如何限制虚拟线程并发数?
A:虚拟线程本身不限流。限流应在业务层用信号量(Semaphore)、Resilience4j 或网关层实现。
小结
Java 21 虚拟线程是 I/O 密集 Java 后端的重要升级。通过 Spring Boot 3.2 的 spring.threads.virtual.enabled=true,你可以零侵入启用虚拟线程,用同步代码风格获得百万级并发能力。关键要点:
- 虚拟线程适合 I/O 密集,不适合 CPU 密集
- 避免
synchronized导致 pin,改用ReentrantLock - 注意连接池、ThreadLocal 等配套调整
- 用 JFR 和 Micrometer 持续监控
下一步建议学习 结构化并发(Structured Concurrency) 与 Scoped Values,它们与虚拟线程配合可构建更安全的并发程序。