关键词:Java 21、虚拟线程、Virtual Threads、Project Loom、Spring Boot 3.x、高并发、线程池、WebFlux、Tomcat、Undertow、WebClient、响应式编程替代方案
摘要:Java 21 正式引入虚拟线程(JEP 444),这是自 Java 诞生以来并发模型的最大变革。本文结合 Spring Boot 3.x 实战,深入讲解虚拟线程的原理、启用方式、性能调优,以及与传统线程池、WebFlux 的对比,帮助你在微服务场景下构建真正的高并发系统。
一、为什么需要虚拟线程?
1.1 传统线程模型的瓶颈
在 Java 传统线程模型中,每个 平台线程(Platform Thread) 都直接映射到操作系统线程(OS Thread)。这种 1:1 映射 带来了严重的资源瓶颈:
| 问题 | 说明 |
|---|---|
| 内存占用大 | 每个 OS 线程默认栈内存约 512KB~1MB,1000 线程 ≈ 1GB 内存 |
| 创建成本高 | OS 线程创建/销毁涉及内核调用,耗时数毫秒 |
| 上下文切换 | 线程数超过 CPU 核心数后,频繁切换带来性能损耗 |
| 阻塞浪费 | I/O 等待期间线程挂起,CPU 资源白白浪费 |
传统解决方案是 线程池 + 异步编程(CompletableFuture / WebFlux),但这引入了回调地狱和复杂的编程模型。
1.2 Project Loom 的答案:虚拟线程
虚拟线程(Virtual Threads) 是 JVM 层面管理的轻量级线程,不直接对应 OS 线程:
应用程序
├── 虚拟线程 1 ──┐
├── 虚拟线程 2 ──┤──► 平台线程(载体线程池,数量 ≈ CPU 核心数)──► OS 线程
├── 虚拟线程 3 ──┘
└── ...(可创建数百万个)
核心特性:
- 极低内存占用:初始栈约 几 KB,按需增长
- 百万级并发:轻松创建 100 万+ 虚拟线程
- 同步写法,异步执行:I/O 阻塞时自动 卸载(unmount) 载体线程,不阻塞 OS 线程
- 兼容现有代码:
ThreadAPI 完全兼容,无需改造
1.3 发展历程
| Java 版本 | 状态 |
|---|---|
| Java 19 | JEP 425:预览特性 |
| Java 20 | JEP 436:二次预览 |
| Java 21 | JEP 444:正式 GA(生产可用) |
| Java 21+ | Spring Boot 3.2+ 原生支持 |
二、虚拟线程核心原理
2.1 线程状态与调度
虚拟线程由 JVM 的 ForkJoinPool 调度器 管理(默认并行度 = CPU 核心数):
虚拟线程状态机:
NEW ──► RUNNABLE ──► 挂载到载体线程 ──► RUNNING
│
I/O 阻塞 / park()
│
从载体线程卸载(unmount)
│
WAITING/BLOCKED
│
I/O 完成 / unpark()
│
重新挂载到(可能不同的)载体线程
│
RUNNING ──► TERMINATED
关键机制:
- 挂载(mount):虚拟线程绑定到某个载体线程执行
- 卸载(unmount):遇到阻塞操作时,从载体线程摘除,载体线程可服务其他虚拟线程
- 续体(Continuation):保存虚拟线程的调用栈,待恢复时继续执行
2.2 钉住(Pinning)问题
虚拟线程在以下情况会被 钉住(pinned) 到载体线程,无法卸载(性能退化为平台线程):
// ❌ 场景1:synchronized 块内执行阻塞操作
synchronized (lock) {
someBlockingIO(); // 虚拟线程被 pinned!
}
// ❌ 场景2:执行 native 方法
// JNI 调用期间无法卸载
解决方案:将 synchronized 替换为 ReentrantLock:
private final ReentrantLock lock = new ReentrantLock();
// ✅ 正确做法
lock.lock();
try {
someBlockingIO(); // 可正常卸载
} finally {
lock.unlock();
}
💡 JDK 24 已着手解决
synchronized的 pinning 问题(JEP 491),未来版本将彻底消除此限制。
2.3 不适合虚拟线程的场景
| 场景 | 说明 |
|---|---|
| CPU 密集型任务 | 无 I/O 阻塞,切换开销反而增加,应使用平台线程池 |
| ThreadLocal 滥用 | 大量 ThreadLocal 对象随虚拟线程创建/销毁,GC 压力大 |
| 对象池模式 | 虚拟线程廉价,无需池化,池化反而引入竞争 |
三、Spring Boot 3.x 集成虚拟线程
3.1 环境准备
<!-- pom.xml -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.5</version>
</parent>
<properties>
<java.version>21</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 或使用 WebFlux -->
<!-- <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency> -->
</dependencies>
<!-- 编译配置 -->
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<release>21</release>
</configuration>
</plugin>
</plugins>
</build>
3.2 一键启用(推荐方式)
Spring Boot 3.2+ 只需一行配置,无需任何代码改动:
# application.yml
spring:
threads:
virtual:
enabled: true # 🚀 开启虚拟线程,Tomcat/Jetty 自动切换
启用后效果:
- Tomcat 使用虚拟线程处理每个 HTTP 请求
- @Async 使用虚拟线程执行
- Spring MVC 调度器 使用虚拟线程
3.3 手动配置方式(精细控制)
@Configuration
public class VirtualThreadConfig {
/**
* Tomcat 使用虚拟线程处理请求
*/
@Bean
public TomcatProtocolHandlerCustomizer<?> tomcatVirtualThreadsProtocolHandlerCustomizer() {
return protocolHandler ->
protocolHandler.setExecutor(Executors.newVirtualThreadPerTaskExecutor());
}
/**
* @Async 异步任务使用虚拟线程
*/
@Bean(name = "virtualThreadExecutor")
public Executor virtualThreadExecutor() {
return Executors.newVirtualThreadPerTaskExecutor();
}
/**
* Spring MVC 任务调度器使用虚拟线程
*/
@Bean
public AsyncTaskExecutor applicationTaskExecutor() {
return new TaskExecutorAdapter(Executors.newVirtualThreadPerTaskExecutor());
}
}
3.4 验证虚拟线程是否生效
@RestController
@RequestMapping("/api/v1/thread")
public class ThreadInfoController {
@GetMapping("/info")
public Map<String, Object> threadInfo() {
Thread current = Thread.currentThread();
return Map.of(
"threadName", current.getName(),
"isVirtual", current.isVirtual(), // ✅ 核心检查点
"threadId", current.threadId(),
"isDaemon", current.isDaemon(),
"activeThreadCount", Thread.activeCount()
);
}
@GetMapping("/stress")
public String stressTest() throws InterruptedException {
// 创建 10000 个虚拟线程
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 10_000; i++) {
executor.submit(() -> {
Thread.sleep(Duration.ofMillis(100)); // 模拟 I/O 等待
return Thread.currentThread().isVirtual();
});
}
}
return "10000 virtual threads completed!";
}
}
访问 GET /api/v1/thread/info 返回:
{
"threadName": "tomcat-handler-0",
"isVirtual": true,
"threadId": 28,
"isDaemon": true,
"activeThreadCount": 42
}
四、实战:虚拟线程 + 数据库连接池调优
4.1 问题:连接池成为新瓶颈
虚拟线程可以创建百万个,但数据库连接池(如 HikariCP)默认只有 10 个连接。如果并发请求远超连接数,虚拟线程会在等待连接时被 pinned(HikariCP 使用 synchronized)。
10,000 虚拟线程 → 争抢 10 个 DB 连接 → HikariCP synchronized 导致 pinning → 退化为平台线程性能
4.2 解决方案:合理调整连接池大小
# application.yml
spring:
datasource:
url: jdbc:mysql://localhost:3306/mydb?serverTimezone=Asia/Shanghai
username: root
password: root123
hikari:
# 虚拟线程场景下,连接池大小建议 = 预期并发 I/O 数
# 公式:pool-size ≈ (核心数 * 2) 到 (预期并发 DB 操作数)
maximum-pool-size: 50 # 根据 DB 服务器承载能力调整
minimum-idle: 10
connection-timeout: 3000
idle-timeout: 600000
max-lifetime: 1800000
# 关键:将 HikariCP 内部锁改为非阻塞
# Spring Boot 3.2 已自动处理部分问题
4.3 使用 R2DBC(响应式数据库驱动)彻底解决 pinning
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-r2dbc</artifactId>
</dependency>
<dependency>
<groupId>io.asyncer</groupId>
<artifactId>r2dbc-mysql</artifactId>
<version>1.1.3</version>
</dependency>
// Repository 层(R2DBC 天然非阻塞)
public interface UserRepository extends ReactiveCrudRepository<User, Long> {
Flux<User> findByStatus(String status);
}
// Service 层(配合虚拟线程 block 调用)
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
// 虚拟线程中 block() 不会浪费 OS 线程
public List<User> getActiveUsers() {
return userRepository.findByStatus("ACTIVE")
.collectList()
.block(); // 虚拟线程阻塞等待,OS 线程被释放
}
}
五、实战:高并发 HTTP 客户端
5.1 HttpClient(JDK 11+)与虚拟线程
@Service
public class ExternalApiService {
private final HttpClient httpClient = HttpClient.newBuilder()
.executor(Executors.newVirtualThreadPerTaskExecutor()) // 使用虚拟线程
.connectTimeout(Duration.ofSeconds(10))
.build();
/**
* 并发调用多个外部 API(虚拟线程版本,同步写法)
*/
public List<String> fetchMultipleApis(List<String> urls) {
return urls.parallelStream()
.map(url -> {
try {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(url))
.timeout(Duration.ofSeconds(5))
.GET()
.build();
return httpClient.send(request, HttpResponse.BodyHandlers.ofString())
.body();
} catch (Exception e) {
return "ERROR: " + e.getMessage();
}
})
.collect(Collectors.toList());
}
}
5.2 RestClient(Spring Boot 3.2 新 API)+ 虚拟线程
@Configuration
public class RestClientConfig {
@Bean
public RestClient restClient(RestClient.Builder builder) {
return builder
.baseUrl("https://api.example.com")
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.build();
}
}
@Service
public class ProductService {
@Autowired
private RestClient restClient;
/**
* 在虚拟线程中同步调用,简洁优雅
*/
public Product getProduct(Long id) {
return restClient.get()
.uri("/products/{id}", id)
.retrieve()
.body(Product.class); // 虚拟线程阻塞等待 I/O,不占用 OS 线程
}
/**
* 并发获取多个商品
*/
public List<Product> getProducts(List<Long> ids) {
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
List<Future<Product>> futures = ids.stream()
.map(id -> executor.submit(() -> getProduct(id)))
.toList();
return futures.stream()
.map(future -> {
try {
return future.get(10, TimeUnit.SECONDS);
} catch (Exception e) {
throw new RuntimeException(e);
}
})
.toList();
}
}
}
六、实战:@Async 异步任务升级
6.1 传统 @Async 的局限
// ❌ 传统方式:依赖固定线程池,高并发时线程耗尽
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean
public Executor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(20);
executor.setMaxPoolSize(100); // 上限 100 线程
executor.setQueueCapacity(500);
return executor;
}
}
6.2 虚拟线程版 @Async
// ✅ 虚拟线程方式:无上限并发,低资源占用
@Configuration
@EnableAsync
public class VirtualThreadAsyncConfig {
@Bean(name = AsyncExecutionAspectSupport.DEFAULT_TASK_EXECUTOR_BEAN_NAME)
public Executor asyncExecutor() {
return Executors.newVirtualThreadPerTaskExecutor();
}
}
@Service
public class NotificationService {
@Async // 自动使用虚拟线程执行器
public CompletableFuture<Void> sendEmailAsync(String to, String content) {
// 模拟发送邮件(I/O 阻塞操作)
log.info("Sending email to {} on thread: {}, virtual: {}",
to,
Thread.currentThread().getName(),
Thread.currentThread().isVirtual()); // true
Thread.sleep(200); // 虚拟线程等待,不阻塞 OS 线程
return CompletableFuture.completedFuture(null);
}
@Async
public CompletableFuture<Void> sendBatchNotifications(List<String> recipients) {
recipients.forEach(r -> sendEmailAsync(r, "Hello!"));
return CompletableFuture.completedFuture(null);
}
}
七、虚拟线程 vs 传统线程池 vs WebFlux 对比
7.1 性能对比(I/O 密集型场景)
| 方案 | 吞吐量(req/s) | 内存占用 | 代码复杂度 |
|---|---|---|---|
| 平台线程池(200线程) | ~2,000 | 200MB+ | 低 |
| 虚拟线程 | ~50,000 | ~50MB | 低 |
| WebFlux(响应式) | ~60,000 | ~40MB | 高 |
| WebFlux + 虚拟线程 | ~65,000 | ~45MB | 高 |
测试环境:8核16G,模拟每个请求 100ms I/O 延迟,1000 并发用户
7.2 选型建议
我的服务是 I/O 密集型(数据库查询、HTTP调用、文件读写)?
├── 是 ──► 虚拟线程 ✅(简单)或 WebFlux(极致性能)
└── 否(CPU密集型:加解密、图像处理、计算)──► 平台线程池 ✅
我的团队熟悉响应式编程?
├── 是 ──► WebFlux 性能略优,但虚拟线程维护更简单
└── 否 ──► 虚拟线程 ✅(同步代码,零学习成本)
现有项目要改造?
├── Spring MVC 项目 ──► 加一行配置,虚拟线程 ✅ 零改造
└── WebFlux 项目 ──► 继续用,无需切换
八、性能监控与调优
8.1 JDK Flight Recorder 监控虚拟线程
# 启动时开启 JFR 录制
java -XX:StartFlightRecording=duration=60s,filename=vthread.jfr \
-Djdk.tracePinnedThreads=full \ # 输出 pinning 警告
-jar app.jar
# 分析 JFR 文件(使用 JDK Mission Control)
jmc vthread.jfr
8.2 检测 Pinning 问题
# JVM 参数:打印 pinned 虚拟线程的堆栈
-Djdk.tracePinnedThreads=full # 完整堆栈
-Djdk.tracePinnedThreads=short # 简短日志
启动后,如果存在 pinning 问题,控制台会输出:
Thread[#28,ForkJoinPool-1-worker-1,5,CarrierThreads]
java.base/java.lang.VirtualThread$VThreadContinuation.onPinned(VirtualThread.java:185)
com.example.service.UserService.queryUser(UserService.java:42) ← 问题代码位置
8.3 Micrometer 指标监控
@Configuration
public class VirtualThreadMetricsConfig {
@Bean
public MeterBinder virtualThreadMetrics() {
return registry -> {
// 自定义虚拟线程计数器
AtomicLong virtualThreadCount = new AtomicLong(0);
Gauge.builder("jvm.virtual.threads.active", virtualThreadCount, AtomicLong::get)
.description("Active virtual thread count")
.register(registry);
};
}
}
# 暴露 Actuator 端点
management:
endpoints:
web:
exposure:
include: health,metrics,prometheus
metrics:
tags:
application: ${spring.application.name}
九、常见坑与最佳实践
9.1 禁止对虚拟线程使用对象池
// ❌ 错误:虚拟线程廉价,不需要池化
ExecutorService pool = new ThreadPoolExecutor(10, 100, ...);
// ✅ 正确:每个任务一个虚拟线程
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
9.2 谨慎使用 ThreadLocal
// ⚠️ 风险:虚拟线程数量庞大,ThreadLocal 导致内存泄漏
private static final ThreadLocal<Connection> connectionHolder = new ThreadLocal<>();
// ✅ 替代方案:Java 20+ ScopedValue(JEP 429,Java 21 预览)
static final ScopedValue<Connection> CONNECTION = ScopedValue.newInstance();
ScopedValue.where(CONNECTION, getConnection())
.run(() -> {
// 在此作用域内安全访问
Connection conn = CONNECTION.get();
});
9.3 避免在虚拟线程中使用 synchronized
// ❌ 导致 pinning,性能退化
synchronized (this) {
jdbcTemplate.query(...); // I/O 阻塞 + pinned = 平台线程性能
}
// ✅ 替换为 ReentrantLock
private final ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
jdbcTemplate.query(...);
} finally {
lock.unlock();
}
9.4 最佳实践总结
| 实践 | 建议 |
|---|---|
| 线程命名 | Thread.ofVirtual().name("vt-task-", 0).factory() 方便调试 |
| 异常处理 | 虚拟线程异常不会自动打印,需显式设置 Thread.UncaughtExceptionHandler |
| CPU 密集型 | 使用独立的平台线程池,避免占用 ForkJoinPool 载体线程 |
| 数据库连接池 | 适当增大连接池大小,避免连接成为瓶颈 |
| 日志 MDC | MDC 基于 ThreadLocal,虚拟线程下需在任务开始时显式复制 MDC |
十、完整项目示例
10.1 MDC 在虚拟线程中的传递
@Component
public class VirtualThreadMDCTaskDecorator implements TaskDecorator {
@Override
public Runnable decorate(Runnable runnable) {
// 捕获父线程的 MDC 上下文
Map<String, String> mdcContext = MDC.getCopyOfContextMap();
return () -> {
try {
if (mdcContext != null) {
MDC.setContextMap(mdcContext); // 传递到虚拟线程
}
runnable.run();
} finally {
MDC.clear();
}
};
}
}
@Configuration
@EnableAsync
public class AsyncConfig {
@Autowired
private VirtualThreadMDCTaskDecorator decorator;
@Bean
public Executor asyncExecutor() {
var factory = Thread.ofVirtual()
.name("async-vt-", 0)
.factory();
// 包装为支持 MDC 的执行器
return new TaskExecutorAdapter(task ->
Thread.ofVirtual().start(decorator.decorate(task)));
}
}
10.2 完整 Controller 示例
@RestController
@RequestMapping("/api/v1/orders")
@RequiredArgsConstructor
@Slf4j
public class OrderController {
private final OrderService orderService;
/**
* 并发查询订单详情(聚合多个微服务数据)
* 虚拟线程让同步代码达到异步性能
*/
@GetMapping("/{orderId}/detail")
public OrderDetailVO getOrderDetail(@PathVariable Long orderId) {
log.info("处理请求,线程: {}, 虚拟线程: {}",
Thread.currentThread().getName(),
Thread.currentThread().isVirtual());
// 并发获取:订单基本信息 + 商品详情 + 用户信息 + 物流状态
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
Future<Order> orderFuture = executor.submit(() -> orderService.getOrder(orderId));
Future<List<Product>> productsFuture = executor.submit(() -> orderService.getProducts(orderId));
Future<UserInfo> userFuture = executor.submit(() -> orderService.getUserInfo(orderId));
Future<LogisticsInfo> logisticsFuture = executor.submit(() -> orderService.getLogistics(orderId));
// 等待所有结果(虚拟线程阻塞,不占用 OS 线程)
return OrderDetailVO.builder()
.order(orderFuture.get(5, TimeUnit.SECONDS))
.products(productsFuture.get(5, TimeUnit.SECONDS))
.user(userFuture.get(5, TimeUnit.SECONDS))
.logistics(logisticsFuture.get(5, TimeUnit.SECONDS))
.build();
} catch (Exception e) {
throw new BusinessException("获取订单详情失败", e);
}
}
}
十一、总结
Java 21 虚拟线程是 高并发编程的范式革命:
| 维度 | 传统方案 | 虚拟线程 |
|---|---|---|
| 并发模型 | 线程池(有上限) | 无限轻量级线程 |
| 代码风格 | 异步回调 / 响应式 | 同步顺序代码 |
| 学习成本 | 高(WebFlux) | 低(与传统线程一致) |
| I/O 性能 | 受线程数限制 | 接近响应式 |
| 迁移成本 | — | 极低(一行配置) |
适用场景
✅ 强烈推荐:
- REST API 服务(大量 HTTP I/O)
- 数据库查询密集型服务
- 微服务间 RPC 调用
- 批量数据处理(文件、消息队列消费)
⚠️ 谨慎使用:
- CPU 密集型计算(加解密、编码转换)
- 严重依赖
synchronized的遗留代码(需先解决 pinning)
Spring Boot 3.2+ 一行配置(spring.threads.virtual.enabled=true)即可享受虚拟线程的性能红利,是目前提升 Java 微服务吞吐量最低成本的方案,强烈推荐在生产环境中采用。
💬 互动:你的项目中有没有遇到线程池耗尽导致的性能瓶颈?欢迎在评论区分享你的经验!
🔗 参考资料:
评论区