虚拟线程用在哪儿


虚拟线程在 Java / Spring Boot 项目里到底怎么写、怎么用、怎么替代传统线程、解决了哪些老问题

环境要求:

  • JDK 21+
  • Spring Boot 3.2.0+(全自动支持虚拟线程)

一、先回顾:传统线程 / 线程池到底有什么问题?

  1. 平台线程 = OS 线程,创建成本极高,不敢多开;
  2. 线程池满了就拒绝任务 / 阻塞,高并发 IO 场景很容易打满;
  3. IO 阻塞(DB、HTTP、Redis、文件)时,线程空占着不释放,资源浪费;
  4. 线程池参数(核心线程、最大线程、队列)极难调优
  5. 大量线程导致 CPU 上下文切换暴涨,性能下降。

虚拟线程就是为解决上面所有问题而生的。


二、JDK21 原生:虚拟线程怎么写?(纯 Java 代码)

虚拟线程不需要线程池,来一个任务开一个虚拟线程,百万线程也不会 OOM。

1. 最简单:直接启动虚拟线程

// 传统写法:启动平台线程
new Thread(() -> {
    // 业务逻辑
}).start();

// 虚拟线程写法(JDK21)
Thread.startVirtualThread(() -> {
    // 你的业务:DB查询、HTTP调用、文件读写
    System.out.println("虚拟线程运行:" + Thread.currentThread().getName());
});

2. 推荐:使用 Executor(最适合替换原有线程池)

// 传统线程池(有上限、会满、会阻塞)
ExecutorService traditionalPool = Executors.newFixedThreadPool(20);

// 虚拟线程执行器(无上限、轻量、自动复用平台线程)
ExecutorService virtualExecutor = Executors.newVirtualThreadPerTaskExecutor();

使用方式和原来完全一样,无痛替换:

// 提交任务
virtualExecutor.submit(() -> {
    // IO 密集型业务:查询数据库、调用第三方接口、导出文件
    userMapper.selectList(null);
});

3. 自定义虚拟线程工厂

ThreadFactory factory = Thread.ofVirtual()
        .name("my-virtual-thread-", 0)
        .factory();

// 使用工厂创建
Thread t = factory.newThread(() -> {
    System.out.println("自定义名称虚拟线程");
});
t.start();

三、Spring Boot 3.2+:全自动使用虚拟线程(90% 场景用这个)

不需要改任何 Controller、Service 业务代码,只加一行配置。

1. 开启 Web 容器虚拟线程(Tomcat/Jetty 全部用虚拟线程处理请求)

spring:
  threads:
    virtual:
      enabled: true  # 开启虚拟线程

效果:

  • 每一个 HTTP 请求,都会用一个虚拟线程处理
  • 原来的代码完全不动
  • 阻塞在 DB/Redis/HTTP/Feign/MinIO 时,自动释放平台线程
  • 高并发下不会再出现 “线程池耗尽” 问题

2. Spring @Async 异步方法使用虚拟线程

如果你用 @Async,默认还是平台线程池,手动替换成虚拟线程:

@Configuration
@EnableAsync
public class AsyncConfig {
    @Bean
    public Executor taskExecutor() {
        // 直接返回虚拟线程执行器,替换原有 ThreadPoolTaskExecutor
        return Executors.newVirtualThreadPerTaskExecutor();
    }
}

业务代码完全不变:

@Service
public class ExportService {
    // 异步导出订单(虚拟线程执行)
    @Async
    public void asyncExportOrder() {
        // 查询DB + 生成文件 + 上传MinIO
        List<Order> list = orderMapper.selectList(null);
        writeToCsv(list);
    }
}

四、真实业务场景:虚拟线程怎么解决传统问题?

场景 1:高并发 Web 接口(查询 DB + Redis)

传统问题:

  • 200 个并发请求 → 200 个平台线程被占满
  • 后续请求直接拒绝或排队

虚拟线程:

@RestController
@RequestMapping("/user")
public class UserController {
    @Autowired
    private UserMapper userMapper;

    @GetMapping("/get")
    public User getUser(Long id) {
        // 虚拟线程内执行
        // DB IO 阻塞 → 自动释放平台线程
        return userMapper.selectById(id);
    }
}

解决的问题:

  • 1 万并发请求也不会 OOM、不会线程池满
  • IO 阻塞时平台线程被复用,吞吐量提升数倍~十几倍

场景 2:大批量异步导出(订单 / 用户导出)

传统问题:

  • 线程池设 10 → 只能同时导出 10 个
  • 线程池设 50 → 内存飙升、CPU 上下文切换爆炸

虚拟线程写法:

@Autowired
private Executor virtualExecutor;

public void batchExport(List<Long> userIds) {
    for (Long userId : userIds) {
        // 来一个任务开一个虚拟线程,无压力
        virtualExecutor.submit(() -> {
            // 查询 + 生成文件 + 上传MinIO
            exportUserFile(userId);
        });
    }
}

解决的问题:

  • 同时启动上千导出任务也稳定
  • 不占用大量平台线程
  • 不会因为线程池满导致导出失败

场景 3:第三方 HTTP 接口批量调用

List<String> urlList = ...;

for (String url : urlList) {
    virtualExecutor.submit(() -> {
        // 阻塞等待第三方返回
        String result = restTemplate.getForObject(url, String.class);
        saveResult(result);
    });
}

解决的问题:

  • 同步调用也能高并发
  • 不会因为网络慢把线程池拖死

五、虚拟线程究竟解决了传统线程的哪些具体问题?

问题传统平台线程 / 线程池虚拟线程
线程数量上限几百~几千,多了 OOM百万级无压力
IO 阻塞(DB/HTTP/ 文件)线程挂起,空占资源自动挂载,释放平台线程
线程池参数调优极难,经常满 / 拒绝无需线程池,不用调参
内存占用高(每个线程栈 1M)极低(几百字节)
高并发下吞吐量低,容易被阻塞拖死极高,IO 越多优势越大

六、必须记住的使用规则(避免踩坑)

1. 只适合 IO 密集型

  • DB、Redis、HTTP、文件、MQ、网络等待 ✅
  • 大量计算、加密、视频处理(CPU 密集)❌ 不会变快

2. synchronized 不会释放平台线程(大坑)

// 虚拟线程阻塞在这里 → 平台线程被占住,不释放
synchronized (lock) {
    // ...
}

替换方案:高并发路径用 ReentrantLock,虚拟线程阻塞时会让出平台线程

3. ThreadLocal 要慎用

虚拟线程数量巨大,ThreadLocal 会导致内存占用飙升。Spring 内部已经帮你处理了 RequestContext 等,但自己写的要小心。

4. 不要池化虚拟线程

虚拟线程本来就是一次性的,再放进线程池完全违背设计思想。直接用:

Executors.newVirtualThreadPerTaskExecutor()

七、最简落地总结(直接照做)

  1. Spring Boot 项目
    • 升级到 JDK21 + Spring Boot 3.2+
    • 加一行 spring.threads.virtual.enabled=true
    • 原有 Web 接口、Service 代码一行不动
    • 自动获得高吞吐、解决线程池满、阻塞浪费问题
  2. 自定义异步任务
    • Executors.newVirtualThreadPerTaskExecutor()
    • 替换所有传统 ThreadPoolExecutor
  3. 业务场景
    • 订单导出、批量调用、高并发查询、文件上传下载 → 直接上虚拟线程
JAVA-技能点
多线程