Java 21 虚拟线程与 Spring Boot 3 实战教程:从原理到生产落地

2026-06-12 14:22:53

Java 21 虚拟线程与 Spring Boot 3 实战教程

适用读者:熟悉 Java 基础与 Spring Boot,希望在生产环境落地虚拟线程的后端工程师。
预计学习时长:45 分钟 · 含动手练习

学习目标

完成本教程后,你将能够:

  1. 解释虚拟线程与平台线程(Platform Thread)的本质区别及适用场景
  2. 在 Spring Boot 3.2+ 项目中一行配置启用虚拟线程
  3. 识别虚拟线程的「最佳实践」与「反模式」,避免 pin 住载体线程
  4. 使用 JFR、Micrometer 监控虚拟线程的运行状态
  5. 将现有基于线程池的阻塞 I/O 服务迁移到虚拟线程架构

前置知识

  • Java 17+ 语法基础(record、sealed class 等了解即可)
  • Spring Boot 3.x 项目结构与依赖管理(Maven/Gradle)
  • 理解阻塞 I/O 与线程模型的基本关系
  • 了解 ExecutorServiceThreadPoolExecutor 的基本用法

第一章:虚拟线程是什么?

1.1 传统线程模型的瓶颈

在 Java 传统模型中,每个 Thread 对应一个操作系统线程(平台线程)。OS 线程创建成本高(约 1MB 栈空间),且上下文切换开销大。因此高并发 Web 服务通常使用「少量平台线程 + 线程池」处理请求。

当业务以 阻塞 I/O 为主(数据库查询、HTTP 调用、文件读写),线程在等待 I/O 返回期间无法处理其他任务,造成线程池耗尽、请求排队。

1.2 虚拟线程的核心思想

Java 21 正式引入的 虚拟线程(Virtual Threads) 由 JVM 调度,挂载在少量平台线程(载体线程 Carrier Thread)上运行。当虚拟线程执行阻塞操作时,JVM 自动将其「卸载」(unmount),让载体线程去执行其他虚拟线程,从而实现 用同步代码风格写出异步级别的并发能力

关键特性:

特性 平台线程 虚拟线程
创建成本 高(~1MB 栈) 极低(~几 KB)
数量上限 数千 数百万
调度方 OS JVM
适用场景 CPU 密集 I/O 密集

1.3 验证环境

确认 JDK 版本:

java -version
# 应显示 openjdk version "21" 或更高

创建测试项目:

curl https://start.spring.io/starter.zip \
  -d dependencies=web,actuator \
  -d javaVersion=21 \
  -d bootVersion=3.3.0 \
  -o vt-demo.zip && unzip vt-demo.zip -d vt-demo

第二章:Spring Boot 启用虚拟线程

2.1 一行配置启用

Spring Boot 3.2 起内置虚拟线程支持。在 application.yml 中添加:

spring:
  threads:
    virtual:
      enabled: true

此配置会让 Spring MVC 的请求处理、@Async 任务、TaskExecutor 等默认使用虚拟线程。

2.2 验证是否生效

编写测试 Controller:

@RestController
@RequestMapping("/api/demo")
public class ThreadInfoController {

    @GetMapping("/thread")
    public Map<String, Object> threadInfo() {
        Thread t = Thread.currentThread();
        return Map.of(
            "name", t.getName(),
            "virtual", t.isVirtual(),
            "threadId", t.threadId()
        );
    }
}

启动后访问 http://localhost:8080/api/demo/thread,应看到:

{
  "name": "",
  "virtual": true,
  "threadId": 42
}

virtual: true 表示请求由虚拟线程处理。

2.3 手动创建虚拟线程

不依赖 Spring 时,可用 JDK API:

// 方式一:Thread.ofVirtual()
Thread vThread = Thread.ofVirtual()
    .name("worker-", 0)
    .start(() -> System.out.println("Hello from virtual thread"));

// 方式二:Executors.newVirtualThreadPerTaskExecutor()
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    IntStream.range(0, 100_000).forEach(i ->
        executor.submit(() -> {
            Thread.sleep(Duration.ofSeconds(1));
            return i;
        })
    );
} // 自动关闭,等待所有任务完成

第三章:实战——高并发 HTTP 客户端

3.1 场景描述

某订单服务需要并发调用 3 个下游微服务(库存、优惠、物流),每个调用耗时 200ms。使用平台线程池时,1000 并发需要大量线程;虚拟线程可轻松处理。

3.2 实现代码

@Service
public class OrderAggregationService {

    private final RestClient restClient;

    public OrderAggregationService(RestClient.Builder builder) {
        this.restClient = builder.baseUrl("http://downstream").build();
    }

    public OrderDetail aggregate(String orderId) {
        try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
            Future<StockInfo> stockFuture = executor.submit(() ->
                restClient.get().uri("/stock/{id}", orderId)
                    .retrieve().body(StockInfo.class));

            Future<DiscountInfo> discountFuture = executor.submit(() ->
                restClient.get().uri("/discount/{id}", orderId)
                    .retrieve().body(DiscountInfo.class));

            Future<LogisticsInfo> logisticsFuture = executor.submit(() ->
                restClient.get().uri("/logistics/{id}", orderId)
                    .retrieve().body(LogisticsInfo.class));

            return new OrderDetail(
                stockFuture.get(),
                discountFuture.get(),
                logisticsFuture.get()
            );
        } catch (Exception e) {
            throw new ServiceException("聚合失败", e);
        }
    }
}

三个下游调用并行执行,总耗时约等于最慢的一个(~200ms),而非串行的 600ms。

3.3 性能对比实验

使用 wrk 压测:

# 虚拟线程开启
wrk -t4 -c500 -d30s http://localhost:8080/api/orders/12345

# 关闭虚拟线程后对比
# spring.threads.virtual.enabled=false

在 I/O 密集场景下,虚拟线程通常能以更少内存支撑更高并发,且 P99 延迟更稳定。


第四章:陷阱与最佳实践

4.1 避免 synchronized 导致 pin

虚拟线程在 synchronized 块内阻塞时,无法卸载,会 pin 住载体线程,退化为平台线程行为。JDK 21 中建议使用 ReentrantLock 替代:

// 反模式:pin 住载体线程
synchronized (lock) {
    Thread.sleep(1000); // 阻塞期间载体线程被占用
}

// 推荐:使用 ReentrantLock
lock.lock();
try {
    Thread.sleep(1000); // 虚拟线程可正常卸载
} finally {
    lock.unlock();
}

4.2 线程本地变量(ThreadLocal)

虚拟线程数量巨大,滥用 ThreadLocal 可能导致内存泄漏。优先使用 ScopedValue(Java 21 预览)或显式传参。

4.3 不适合虚拟线程的场景

  • CPU 密集计算:图像处理、加密运算、大数据聚合——应使用平台线程池 + 固定大小
  • Native 代码长时间阻塞:JNI 调用不会触发卸载
  • 自定义线程池混用:确保池内全部是虚拟线程或全部是平台线程

4.4 监控指标

启用 Actuator + Micrometer:

management:
  endpoints:
    web:
      exposure:
        include: metrics,threaddump

关注指标:

  • jvm.threads.virtual:当前虚拟线程数
  • jvm.threads.live:存活线程总数
  • 通过 JFR 事件 jdk.VirtualThreadPinned 检测 pin 问题

第五章:从线程池迁移的决策清单

迁移前评估:

  1. 服务是否以阻塞 I/O 为主?(是 → 适合)
  2. 是否大量使用 synchronized?(是 → 先重构为 Lock)
  3. 是否依赖固定大小线程池做限流?(是 → 保留平台线程池做限流,业务逻辑用虚拟线程)
  4. 下游连接池大小是否匹配?(虚拟线程并发高时,需调大 HikariCP、HttpClient 连接池)
# HikariCP 示例:虚拟线程下可适当增大
spring:
  datasource:
    hikari:
      maximum-pool-size: 50

练习 / 作业

  1. 基础练习:创建 Spring Boot 项目,启用虚拟线程,编写接口返回当前线程信息,截图验证 isVirtual() == true
  2. 并发练习:实现一个接口,并发请求 5 个公开 HTTP API(如 GitHub API),聚合结果返回。对比虚拟线程开启前后的压测数据。
  3. 排障练习:故意在 synchronized 块内 Thread.sleep(5000),用 JFR 录制并查找 VirtualThreadPinned 事件。
  4. 进阶作业:将团队内一个使用 @Async + 固定线程池的服务迁移到虚拟线程,记录内存与延迟变化,撰写迁移报告。

FAQ

Q:虚拟线程能替代响应式编程(WebFlux)吗?

A:在多数 I/O 密集场景可以。虚拟线程让你用同步代码获得类似吞吐,且调试更简单。但若团队已深度投入 Reactor 生态,迁移成本需评估。

Q:Spring Boot 2.x 能用虚拟线程吗?

A:需要 Spring Boot 3.2+。2.x 可手动使用 Executors.newVirtualThreadPerTaskExecutor(),但无法一行启用 MVC 虚拟线程。

Q:虚拟线程与 Goroutine 一样吗?

A:理念相似,但实现不同。Go 的 goroutine 是语言级调度;Java 虚拟线程是 JVM 级,与现有 Java 生态无缝兼容。

Q:生产环境 JDK 21 稳定吗?

A:JDK 21 是 LTS 版本,虚拟线程已正式发布(非预览)。Spring Boot 3.2+ 官方支持,国内已有大量生产案例。

Q:如何限制虚拟线程并发数?

A:虚拟线程本身不限流。限流应在业务层用信号量(Semaphore)、Resilience4j 或网关层实现。


小结

Java 21 虚拟线程是 I/O 密集 Java 后端的重要升级。通过 Spring Boot 3.2 的 spring.threads.virtual.enabled=true,你可以零侵入启用虚拟线程,用同步代码风格获得百万级并发能力。关键要点:

  • 虚拟线程适合 I/O 密集,不适合 CPU 密集
  • 避免 synchronized 导致 pin,改用 ReentrantLock
  • 注意连接池、ThreadLocal 等配套调整
  • 用 JFR 和 Micrometer 持续监控

下一步建议学习 结构化并发(Structured Concurrency)Scoped Values,它们与虚拟线程配合可构建更安全的并发程序。