虚拟线程
虚拟线程(Virtual Threads)是 Java 21 正式引入的轻量级线程实现(Project Loom),由 JVM 而非操作系统调度。核心价值是用同步代码的写法获得异步代码的吞吐量,特别适合 I/O 密集型的 Web 服务。
平台线程 vs 虚拟线程
| 对比项 | 平台线程(OS Thread) | 虚拟线程(Virtual Thread) |
|---|---|---|
| 创建成本 | 高(~1MB 栈,系统调用) | 极低(几百字节,JVM 管理) |
| 数量上限 | 数千(受 OS 限制) | 数百万 |
| 阻塞代价 | 占用 OS 线程 | 自动 unmount,不占 OS 线程 |
| 调度者 | 操作系统 | JVM(基于 ForkJoinPool) |
| 适合场景 | CPU 密集型 | I/O 密集型 |
| API 兼容性 | — | 完全兼容 Thread API |
平台线程在执行阻塞 I/O 时会挂起整个 OS 线程,而虚拟线程被阻塞时会从 carrier thread(平台线程)上 unmount,让出 carrier thread 去执行其他虚拟线程,I/O 完成后再 remount 继续执行。
创建虚拟线程
// 方式一:Thread.ofVirtual()
Thread vt = Thread.ofVirtual()
.name("my-vt")
.start(() -> System.out.println("Hello from virtual thread"));
// 方式二:Thread.startVirtualThread(最简)
Thread.startVirtualThread(() -> doWork());
// 方式三:虚拟线程工厂
ThreadFactory factory = Thread.ofVirtual().name("worker-", 0).factory();
Thread t = factory.newThread(() -> doWork());
t.start();
// 方式四:专用 ExecutorService(每个任务一个虚拟线程)
try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
executor.submit(() -> fetchFromDb());
executor.submit(() -> callExternalApi());
} // try-with-resources 自动 shutdownSpring Boot 集成
Spring Boot 3.2+ 一键开启
spring:
threads:
virtual:
enabled: true # 开启后,Tomcat / Jetty / Undertow 全部切换到虚拟线程开启后 Spring Boot 自动完成以下配置:
TomcatProtocolHandlerCustomizer:Tomcat 请求线程切换为虚拟线程JettyVirtualThreadsConnectionFactory:Jetty 连接使用虚拟线程@Async默认执行器切换为虚拟线程SimpleAsyncTaskExecutor默认使用虚拟线程
手动配置(Spring Boot 3.0 / 3.1 或需要细粒度控制)
@Configuration
public class VirtualThreadConfig {
// Web 请求线程池改为虚拟线程
@Bean
public TomcatProtocolHandlerCustomizer<?> virtualThreadTomcat() {
return handler -> handler.setExecutor(
Executors.newVirtualThreadPerTaskExecutor()
);
}
// @Async 执行器改为虚拟线程
@Bean(name = "virtualThreadExecutor")
public AsyncTaskExecutor virtualThreadExecutor() {
return new TaskExecutorAdapter(
Executors.newVirtualThreadPerTaskExecutor()
);
}
// 定时任务线程池改为虚拟线程
@Bean
public TaskScheduler taskScheduler() {
SimpleAsyncTaskScheduler scheduler = new SimpleAsyncTaskScheduler();
scheduler.setVirtualThreads(true);
return scheduler;
}
}
// 启用异步
@SpringBootApplication
@EnableAsync
public class Application {}// 使用虚拟线程异步执行
@Service
public class ReportService {
@Async("virtualThreadExecutor")
public CompletableFuture<Report> generateReport(Long id) {
// 耗时 I/O 操作,虚拟线程在等待时自动让出 carrier thread
Report report = heavyDbQuery(id);
return CompletableFuture.completedFuture(report);
}
}性能提升场景
I/O 密集型(虚拟线程收益显著)
@RestController
@RequiredArgsConstructor
public class OrderController {
private final OrderService orderService;
private final InventoryService inventoryService;
private final UserService userService;
// 开启虚拟线程后,即使是同步代码,高并发下也不会耗尽线程池
@GetMapping("/orders/{id}/detail")
public OrderDetail getDetail(@PathVariable Long id) {
// 三次数据库查询串行执行,每次阻塞时虚拟线程自动让出 carrier thread
Order order = orderService.getById(id);
User user = userService.getById(order.getUserId());
Inventory inv = inventoryService.getByProductId(order.getProductId());
return OrderDetail.of(order, user, inv);
}
}并行 I/O(结构化并发)
// Java 21 结构化并发(预览特性):并行发起多个 I/O,等待全部完成
public OrderDetail getDetailParallel(Long id) throws InterruptedException, ExecutionException {
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
Subtask<Order> orderTask = scope.fork(() -> orderService.getById(id));
Subtask<User> userTask = scope.fork(() -> userService.getById(id));
Subtask<Inventory> invTask = scope.fork(() -> inventoryService.getByOrderId(id));
scope.join().throwIfFailed(); // 等待全部完成,任一失败则取消其余
return OrderDetail.of(orderTask.get(), userTask.get(), invTask.get());
}
}限制与注意事项
1. synchronized 导致 pinning(固定)
虚拟线程在执行 synchronized 块时无法 unmount,会将 carrier thread 一起阻塞(称为 pinning):
// 危险:synchronized 内有阻塞 I/O,导致 carrier thread 被 pin 住
public synchronized void dangerousMethod() {
String result = jdbcTemplate.queryForObject("SELECT ...", String.class); // I/O 阻塞
}
// 解决方案:用 ReentrantLock 替换 synchronized
private final ReentrantLock lock = new ReentrantLock();
public void safeMethod() {
lock.lock();
try {
String result = jdbcTemplate.queryForObject("SELECT ...", String.class);
} finally {
lock.unlock();
}
}检测 pinning 事件(JVM 参数):
# 输出 pinning 事件到控制台
java -Djdk.tracePinnedThreads=full -jar app.jar
# 或 short 模式(只显示锁相关堆栈)
java -Djdk.tracePinnedThreads=short -jar app.jarSpring Boot 3.2 已将内部
synchronized替换为ReentrantLock,常用驱动(如 PostgreSQL JDBC 42.7+、MySQL Connector/J 9.0+)也完成了适配。
2. ThreadLocal 的影响
虚拟线程数量可达百万,若大量使用 ThreadLocal 存储对象,内存开销会显著增加:
// 潜在问题:每个虚拟线程都持有一份 ThreadLocal 副本
private static final ThreadLocal<HeavyObject> CONTEXT = new ThreadLocal<>();
// 解决:使用 ScopedValue(Java 21 预览)替代 ThreadLocal(只读传递上下文)
static final ScopedValue<User> CURRENT_USER = ScopedValue.newInstance();
public void handleRequest(User user) {
ScopedValue.runWhere(CURRENT_USER, user, () -> {
// 该 scope 内所有代码(包括被调用方法)都能通过 CURRENT_USER.get() 获取
processOrder();
});
}3. CPU 密集型任务不适合
虚拟线程的优势在于阻塞时让出 carrier thread,纯 CPU 计算没有阻塞点,使用虚拟线程与平台线程效果相同,甚至因调度开销略差:
// CPU 密集型:仍用有界线程池
@Bean(name = "cpuExecutor")
public Executor cpuExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(Runtime.getRuntime().availableProcessors());
executor.setMaxPoolSize(Runtime.getRuntime().availableProcessors());
executor.setThreadNamePrefix("cpu-");
executor.initialize();
return executor;
}4. 线程池不适用于虚拟线程
虚拟线程创建成本极低,不应放入线程池复用:
// 错误:把虚拟线程放进固定线程池
ExecutorService wrong = Executors.newFixedThreadPool(100, Thread.ofVirtual().factory());
// 正确:每任务一线程,用完即弃
ExecutorService correct = Executors.newVirtualThreadPerTaskExecutor();与响应式编程对比
| 对比 | 虚拟线程 | WebFlux(响应式) |
|---|---|---|
| 编程模型 | 同步(直观) | 异步(响应式链) |
| 代码可读性 | 高 | 低(学习曲线陡) |
| 调试难度 | 低(栈帧清晰) | 高(调用链复杂) |
| I/O 吞吐量 | 接近响应式 | 最优 |
| CPU 密集型 | 与平台线程相同 | 与平台线程相同 |
| 生态兼容性 | 完全兼容现有库 | 需要响应式驱动 |
| 适用场景 | 新项目或迁移改造 | 极高并发、流处理 |
响应式编程详见 响应式编程。
监控与调试
# 查看虚拟线程信息(jcmd)
jcmd <pid> Thread.dump_to_file -format=json /tmp/threads.json
# JFR 事件监控虚拟线程
java -XX:StartFlightRecording=filename=vt.jfr,settings=profile -jar app.jar// 判断当前线程是否为虚拟线程
boolean isVirtual = Thread.currentThread().isVirtual();
// 获取虚拟线程调度信息
Thread vt = Thread.currentThread();
log.info("isVirtual={}, name={}", vt.isVirtual(), vt.getName());迁移建议
- 升级到 Spring Boot 3.2 + Java 21
- 开启
spring.threads.virtual.enabled=true - 检查 synchronized + I/O 组合,替换为
ReentrantLock - 替换阻塞驱动:确认 JDBC、Redis 客户端版本支持虚拟线程
- 移除不必要的线程池:原来为了异步而设置的线程池可简化为虚拟线程
- 性能测试:I/O 密集型接口压测对比平台线程