虚拟线程

虚拟线程(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 自动 shutdown

Spring 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.jar

Spring 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());

迁移建议

  1. 升级到 Spring Boot 3.2 + Java 21
  2. 开启 spring.threads.virtual.enabled=true
  3. 检查 synchronized + I/O 组合,替换为 ReentrantLock
  4. 替换阻塞驱动:确认 JDBC、Redis 客户端版本支持虚拟线程
  5. 移除不必要的线程池:原来为了异步而设置的线程池可简化为虚拟线程
  6. 性能测试:I/O 密集型接口压测对比平台线程

相关链接