【Spring Boot 3.2 填坑日记】“虚拟线程都配置了,为什么 @Async 还是用线程池?”:Spring 异步任务与虚拟线程的兼容性实战

作者:@Neoest
时间:2026-03-26
关键词:Spring Boot 3.2、虚拟线程、Virtual Threads、@Async、@Scheduled、TaskExecutor、Java 21


1. 问题现场

项目升级到 Java 21 + Spring Boot 3.2,打算体验一把虚拟线程(Virtual Threads)的魅力。按照官方文档,只需简单配置:

spring:
  threads:
    virtual:
      enabled: true

然后写了一个异步方法:

@Service
@Slf4j
public class OrderService {
    
    @Async
    public CompletableFuture<String> processOrder(OrderDTO dto) {
        log.info("Processing order: {}", dto.getOrderNo());
        // 具体业务-调用远程服务、并且操作了数据库
        String accessToken =  client.getAccessTokenInfo();
        return CompletableFuture.completedFuture(accessToken);
    }
}

启动类上加了 @EnableAsync。满怀期待地运行,查看日志,结果:

Processing order: ORD001
Thread: pool-1-thread-1

线程名竟然是 pool-1-thread-1! 这不是传统线程池的名字吗?虚拟线程不应该是 virtual-thread-xxx 吗?

检查配置,spring.threads.virtual.enabled 明明设为 true。查阅文档,发现这个配置只影响 Tomcat 等 Web 容器的请求处理线程,对 @Async 并不生效。

2. 先定位根因

组件默认线程模型配置虚拟线程的方式
Tomcat平台线程池spring.threads.virtual.enabled=true 自动切换为虚拟线程
@AsyncSimpleAsyncTaskExecutor 或自定义 TaskExecutor需要显式配置 AsyncTaskExecutor 使用虚拟线程
@Scheduled单线程池需要配置 ScheduledTaskExecutor 使用虚拟线程
Spring MVC 阻塞执行Tomcat 线程自动跟随 Web 容器配置

根本原因spring.threads.virtual.enabled 是一个作用于 Web 服务器 的配置项,它修改了 TomcatJetty 等容器的请求处理线程工厂。而 @Async 底层依赖的是 Spring 的 TaskExecutor 抽象,默认是 SimpleAsyncTaskExecutor(会为每个任务创建新线程),并不会自动读取该配置。因此,即使 Web 请求已经跑在虚拟线程上,异步任务依然在平台线程池中执行,没有享受到虚拟线程带来的轻量级优势。

3. 五种修复姿势

✅ 方案 A:自定义 AsyncTaskExecutor(最直接)

创建一个配置类,显式声明 AsyncTaskExecutor Bean,并指定使用虚拟线程:

@Configuration
@EnableAsync
public class AsyncConfig {

    @Bean
    public AsyncTaskExecutor asyncTaskExecutor() {
        // Spring Boot 3.2+ 提供了 VirtualThreadTaskExecutor
        return new VirtualThreadTaskExecutor("virtual-async-");
    }
}

VirtualThreadTaskExecutor 是 Spring 6.1+ 提供的虚拟线程执行器实现,内部使用 Executors.newVirtualThreadPerTaskExecutor()。现在再执行 @Async 方法,线程名变为 virtual-async-xxx

✅ 方案 B:自定义 TaskExecutor 并绑定到所有异步任务

如果你想更精细地控制,可以配置 TaskExecutor Bean,Spring 会自动将它用于所有 @Async 任务(如果没有显式指定其他执行器):

@Bean
public Executor taskExecutor() {
    return Executors.newVirtualThreadPerTaskExecutor();
}

但注意:TaskExecutor 接口和 Executor 不同,Spring 的 @Async 会寻找 AsyncTaskExecutor 类型的 Bean。如果只定义 Executor,需要确保它被包装为 AsyncTaskExecutor,或者直接使用 VirtualThreadTaskExecutor

✅ 方案 C:使用 @Bean 方法结合 ThreadPoolTaskExecutor(虚拟线程版)

如果希望保留线程池的某些特性(如任务队列、拒绝策略),又想使用虚拟线程,可以自定义 ThreadPoolTaskExecutor,设置线程工厂为虚拟线程工厂:

@Bean
public ThreadPoolTaskExecutor asyncTaskExecutor() {
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    executor.setThreadFactory(Thread.ofVirtual().name("virtual-", 0).factory());
    executor.setCorePoolSize(10);
    executor.setMaxPoolSize(50);
    executor.setQueueCapacity(100);
    executor.initialize();
    return executor;
}

注意:虚拟线程通常不需要池化,因为创建成本极低。但有时为了控制并发数,可以限制虚拟线程的创建速率,这时池化仍有意义。不过 Spring 官方推荐直接使用 VirtualThreadTaskExecutor,它不设线程数限制,每个任务都新启一个虚拟线程。

✅ 方案 D:让 @Scheduled 也使用虚拟线程

定时任务同样默认使用平台线程。可以通过配置 ScheduledTaskExecutor 来切换:

@Bean
public TaskScheduler taskScheduler() {
    return new ConcurrentTaskScheduler(Executors.newVirtualThreadPerTaskExecutor());
}

✅ 方案 E:利用 Spring Boot 3.2 的自动配置(升级版)

Spring Boot 3.2 引入了 VirtualThreadsAutoConfiguration,它只配置了 ApplicationTaskExecutor 用于 Web 请求处理。如果想自动配置 @Async 的虚拟线程,需要自己编写 starter 或使用社区扩展。但一个简单的做法是:定义一个 AsyncConfigurer 实现:

@Configuration
public class AsyncVirtualThreadConfig implements AsyncConfigurer {
    
    @Override
    public Executor getAsyncExecutor() {
        return new VirtualThreadTaskExecutor("virtual-");
    }
    
    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        return (ex, method, params) -> log.error("Async method error: " + method, ex);
    }
}

同时确保 @EnableAsync 已添加。

4. 源码分析:为什么配置没生效

我们来看 Spring Boot 3.2 中 VirtualThreadsAutoConfiguration 的部分源码:

@AutoConfiguration
@ConditionalOnThreading(Threading.VIRTUAL)
public class VirtualThreadsAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean
    public TomcatProtocolHandlerCustomizer<?> tomcatProtocolHandlerCustomizer() {
        // 只影响 Tomcat 的请求处理线程
        return protocolHandler -> protocolHandler.setExecutor(Executors.newVirtualThreadPerTaskExecutor());
    }

    @Bean
    @ConditionalOnMissingBean
    public JettyVirtualThreadsWebServerFactoryCustomizer jettyVirtualThreadsWebServerFactoryCustomizer() {
        return new JettyVirtualThreadsWebServerFactoryCustomizer();
    }
    // 没有配置任何 TaskExecutor 相关的 Bean
}

可以看到,这个自动配置只修改了 Web 容器的 Executor。而 @Async 的执行器是通过 AsyncExecutionInterceptor 来获取的,它的逻辑在 AsyncExecutionAspectSupport.getDefaultExecutor 中:

protected Executor getDefaultExecutor(BeanFactory beanFactory) {
    if (beanFactory != null) {
        try {
            // 先找名字为 "taskExecutor" 的 Executor
            return beanFactory.getBean("taskExecutor", Executor.class);
        } catch (NoSuchBeanDefinitionException ex) {
            // 再找 AsyncTaskExecutor 类型
            // ...
        }
    }
    // 兜底使用 SimpleAsyncTaskExecutor
    return new SimpleAsyncTaskExecutor();
}

由于我们没有显式提供 taskExecutorAsyncTaskExecutor,所以最终用的是 SimpleAsyncTaskExecutor。这就是为什么配置了虚拟线程但异步任务没切换的原因。

5. 方案对比

方案实现方式是否自动适配所有异步对 @Scheduled 影响复杂度
VirtualThreadTaskExecutor显式配置 Bean是(如果作为唯一 AsyncTaskExecutor)否(需单独配置)
Executors.newVirtualThreadPerTaskExecutor定义 Executor Bean是(需包装为 AsyncTaskExecutor)
自定义 ThreadPoolTaskExecutor + 虚拟线程工厂池化虚拟线程
实现 AsyncConfigurer统一配置异步执行器和异常处理
配置 TaskScheduler独立配置定时任务

推荐组合:对于大多数场景,使用 VirtualThreadTaskExecutor 作为 AsyncTaskExecutor,同时单独配置 TaskScheduler 使用虚拟线程,这样可以同时覆盖 @Async@Scheduled

6. 扩展:虚拟线程在其他场景的适用性

场景虚拟线程效果注意事项
Tomcat 请求处理✅ 自动生效配置 spring.threads.virtual.enabled=true
@Async 异步任务❌ 需手动配置使用 VirtualThreadTaskExecutor
@Scheduled 定时任务❌ 需手动配置使用 ConcurrentTaskScheduler + 虚拟线程执行器
CompletableFuture 自定义⚠️ 需指定执行器CompletableFuture.supplyAsync(..., virtualExecutor)
Spring Cloud Stream 消费者❌ 需配置需设置 spring.cloud.stream.bindings.*.consumer.concurrency 与虚拟线程兼容?
JDBC 阻塞操作✅ 虚拟线程可大量并发但数据库连接池可能成为瓶颈
阻塞 I/O(文件、网络)✅ 典型应用虚拟线程优势场景

7. 一键复制版配置(组合拳)

@Configuration
@EnableAsync
@EnableScheduling
public class VirtualThreadsConfig implements AsyncConfigurer {

    // 1. 配置 @Async 使用虚拟线程
    @Override
    public Executor getAsyncExecutor() {
        return new VirtualThreadTaskExecutor("async-virtual-");
    }

    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        return (throwable, method, params) -> {
            log.error("Uncaught exception in async method: {}", method.getName(), throwable);
        };
    }

    // 2. 配置 @Scheduled 使用虚拟线程
    @Bean
    public TaskScheduler taskScheduler() {
        return new ConcurrentTaskScheduler(Executors.newVirtualThreadPerTaskExecutor());
    }
}

同时确保 application.yml 中已开启 Web 容器的虚拟线程:

spring:
  threads:
    virtual:
      enabled: true

8. 总结与注意事项

  • 虚拟线程不是万能药:如果任务是 CPU 密集型,虚拟线程反而可能导致过多线程竞争 CPU,影响性能。
  • 线程上下文:虚拟线程在切换时不需要保存内核栈,但 ThreadLocal 等依然有效,需注意内存占用。
  • 兼容性:某些第三方库可能使用 Thread.currentThread() 的特定属性,虚拟线程也完全兼容(如 ThreadLocalInheritableThreadLocal)。
  • Spring Boot 版本:虚拟线程支持从 Spring Boot 3.2 开始,之前版本需自己定义 TaskExecutor

9. 参考资料


如果你也踩过虚拟线程的坑,欢迎评论区分享!如果本文让你少走弯路,记得点赞收藏~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Neoest

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值