侧边栏壁纸
  • 累计撰写 86 篇文章
  • 累计创建 31 个标签
  • 累计收到 21 条评论

目 录CONTENT

文章目录

Java 21 虚拟线程(Virtual Threads)+ Spring Boot 3.x 实战:从入门到高并发调优全攻略

Administrator
2026-04-06 / 0 评论 / 0 点赞 / 0 阅读 / 0 字 / 正在检测是否收录...
温馨提示:
部分素材来自网络,若不小心影响到您的利益,请联系我们删除。

关键词:Java 21、虚拟线程、Virtual Threads、Project Loom、Spring Boot 3.x、高并发、线程池、WebFlux、Tomcat、Undertow、WebClient、响应式编程替代方案

摘要:Java 21 正式引入虚拟线程(JEP 444),这是自 Java 诞生以来并发模型的最大变革。本文结合 Spring Boot 3.x 实战,深入讲解虚拟线程的原理、启用方式、性能调优,以及与传统线程池、WebFlux 的对比,帮助你在微服务场景下构建真正的高并发系统。


一、为什么需要虚拟线程?

1.1 传统线程模型的瓶颈

在 Java 传统线程模型中,每个 平台线程(Platform Thread) 都直接映射到操作系统线程(OS Thread)。这种 1:1 映射 带来了严重的资源瓶颈:

问题说明
内存占用大每个 OS 线程默认栈内存约 512KB~1MB,1000 线程 ≈ 1GB 内存
创建成本高OS 线程创建/销毁涉及内核调用,耗时数毫秒
上下文切换线程数超过 CPU 核心数后,频繁切换带来性能损耗
阻塞浪费I/O 等待期间线程挂起,CPU 资源白白浪费

传统解决方案是 线程池 + 异步编程(CompletableFuture / WebFlux),但这引入了回调地狱和复杂的编程模型。

1.2 Project Loom 的答案:虚拟线程

虚拟线程(Virtual Threads) 是 JVM 层面管理的轻量级线程,不直接对应 OS 线程:

应用程序
├── 虚拟线程 1 ──┐
├── 虚拟线程 2 ──┤──► 平台线程(载体线程池,数量 ≈ CPU 核心数)──► OS 线程
├── 虚拟线程 3 ──┘
└── ...(可创建数百万个)

核心特性:

  • 极低内存占用:初始栈约 几 KB,按需增长
  • 百万级并发:轻松创建 100 万+ 虚拟线程
  • 同步写法,异步执行:I/O 阻塞时自动 卸载(unmount) 载体线程,不阻塞 OS 线程
  • 兼容现有代码Thread API 完全兼容,无需改造

1.3 发展历程

Java 版本状态
Java 19JEP 425:预览特性
Java 20JEP 436:二次预览
Java 21JEP 444:正式 GA(生产可用)
Java 21+Spring Boot 3.2+ 原生支持

二、虚拟线程核心原理

2.1 线程状态与调度

虚拟线程由 JVM 的 ForkJoinPool 调度器 管理(默认并行度 = CPU 核心数):

虚拟线程状态机:

NEW ──► RUNNABLE ──► 挂载到载体线程 ──► RUNNING
                                          │
                                    I/O 阻塞 / park()
                                          │
                                    从载体线程卸载(unmount)
                                          │
                                    WAITING/BLOCKED
                                          │
                                    I/O 完成 / unpark()
                                          │
                                    重新挂载到(可能不同的)载体线程
                                          │
                                       RUNNING ──► TERMINATED

关键机制

  • 挂载(mount):虚拟线程绑定到某个载体线程执行
  • 卸载(unmount):遇到阻塞操作时,从载体线程摘除,载体线程可服务其他虚拟线程
  • 续体(Continuation):保存虚拟线程的调用栈,待恢复时继续执行

2.2 钉住(Pinning)问题

虚拟线程在以下情况会被 钉住(pinned) 到载体线程,无法卸载(性能退化为平台线程):

// ❌ 场景1:synchronized 块内执行阻塞操作
synchronized (lock) {
    someBlockingIO(); // 虚拟线程被 pinned!
}

// ❌ 场景2:执行 native 方法
// JNI 调用期间无法卸载

解决方案:将 synchronized 替换为 ReentrantLock

private final ReentrantLock lock = new ReentrantLock();

// ✅ 正确做法
lock.lock();
try {
    someBlockingIO(); // 可正常卸载
} finally {
    lock.unlock();
}

💡 JDK 24 已着手解决 synchronized 的 pinning 问题(JEP 491),未来版本将彻底消除此限制。

2.3 不适合虚拟线程的场景

场景说明
CPU 密集型任务无 I/O 阻塞,切换开销反而增加,应使用平台线程池
ThreadLocal 滥用大量 ThreadLocal 对象随虚拟线程创建/销毁,GC 压力大
对象池模式虚拟线程廉价,无需池化,池化反而引入竞争

三、Spring Boot 3.x 集成虚拟线程

3.1 环境准备

<!-- pom.xml -->
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.2.5</version>
</parent>

<properties>
    <java.version>21</java.version>
</properties>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- 或使用 WebFlux -->
    <!-- <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-webflux</artifactId>
    </dependency> -->
</dependencies>

<!-- 编译配置 -->
<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <configuration>
                <release>21</release>
            </configuration>
        </plugin>
    </plugins>
</build>

3.2 一键启用(推荐方式)

Spring Boot 3.2+ 只需一行配置,无需任何代码改动:

# application.yml
spring:
  threads:
    virtual:
      enabled: true  # 🚀 开启虚拟线程,Tomcat/Jetty 自动切换

启用后效果:

  • Tomcat 使用虚拟线程处理每个 HTTP 请求
  • @Async 使用虚拟线程执行
  • Spring MVC 调度器 使用虚拟线程

3.3 手动配置方式(精细控制)

@Configuration
public class VirtualThreadConfig {

    /**
     * Tomcat 使用虚拟线程处理请求
     */
    @Bean
    public TomcatProtocolHandlerCustomizer<?> tomcatVirtualThreadsProtocolHandlerCustomizer() {
        return protocolHandler ->
            protocolHandler.setExecutor(Executors.newVirtualThreadPerTaskExecutor());
    }

    /**
     * @Async 异步任务使用虚拟线程
     */
    @Bean(name = "virtualThreadExecutor")
    public Executor virtualThreadExecutor() {
        return Executors.newVirtualThreadPerTaskExecutor();
    }

    /**
     * Spring MVC 任务调度器使用虚拟线程
     */
    @Bean
    public AsyncTaskExecutor applicationTaskExecutor() {
        return new TaskExecutorAdapter(Executors.newVirtualThreadPerTaskExecutor());
    }
}

3.4 验证虚拟线程是否生效

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

    @GetMapping("/info")
    public Map<String, Object> threadInfo() {
        Thread current = Thread.currentThread();
        return Map.of(
            "threadName", current.getName(),
            "isVirtual", current.isVirtual(),            // ✅ 核心检查点
            "threadId", current.threadId(),
            "isDaemon", current.isDaemon(),
            "activeThreadCount", Thread.activeCount()
        );
    }

    @GetMapping("/stress")
    public String stressTest() throws InterruptedException {
        // 创建 10000 个虚拟线程
        try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
            for (int i = 0; i < 10_000; i++) {
                executor.submit(() -> {
                    Thread.sleep(Duration.ofMillis(100)); // 模拟 I/O 等待
                    return Thread.currentThread().isVirtual();
                });
            }
        }
        return "10000 virtual threads completed!";
    }
}

访问 GET /api/v1/thread/info 返回:

{
  "threadName": "tomcat-handler-0",
  "isVirtual": true,
  "threadId": 28,
  "isDaemon": true,
  "activeThreadCount": 42
}

四、实战:虚拟线程 + 数据库连接池调优

4.1 问题:连接池成为新瓶颈

虚拟线程可以创建百万个,但数据库连接池(如 HikariCP)默认只有 10 个连接。如果并发请求远超连接数,虚拟线程会在等待连接时被 pinned(HikariCP 使用 synchronized)。

10,000 虚拟线程 → 争抢 10 个 DB 连接 → HikariCP synchronized 导致 pinning → 退化为平台线程性能

4.2 解决方案:合理调整连接池大小

# application.yml
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/mydb?serverTimezone=Asia/Shanghai
    username: root
    password: root123
    hikari:
      # 虚拟线程场景下,连接池大小建议 = 预期并发 I/O 数
      # 公式:pool-size ≈ (核心数 * 2) 到 (预期并发 DB 操作数)
      maximum-pool-size: 50      # 根据 DB 服务器承载能力调整
      minimum-idle: 10
      connection-timeout: 3000
      idle-timeout: 600000
      max-lifetime: 1800000
      # 关键:将 HikariCP 内部锁改为非阻塞
      # Spring Boot 3.2 已自动处理部分问题

4.3 使用 R2DBC(响应式数据库驱动)彻底解决 pinning

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-r2dbc</artifactId>
</dependency>
<dependency>
    <groupId>io.asyncer</groupId>
    <artifactId>r2dbc-mysql</artifactId>
    <version>1.1.3</version>
</dependency>
// Repository 层(R2DBC 天然非阻塞)
public interface UserRepository extends ReactiveCrudRepository<User, Long> {
    Flux<User> findByStatus(String status);
}

// Service 层(配合虚拟线程 block 调用)
@Service
public class UserService {
    
    @Autowired
    private UserRepository userRepository;

    // 虚拟线程中 block() 不会浪费 OS 线程
    public List<User> getActiveUsers() {
        return userRepository.findByStatus("ACTIVE")
                .collectList()
                .block(); // 虚拟线程阻塞等待,OS 线程被释放
    }
}

五、实战:高并发 HTTP 客户端

5.1 HttpClient(JDK 11+)与虚拟线程

@Service
public class ExternalApiService {

    private final HttpClient httpClient = HttpClient.newBuilder()
            .executor(Executors.newVirtualThreadPerTaskExecutor()) // 使用虚拟线程
            .connectTimeout(Duration.ofSeconds(10))
            .build();

    /**
     * 并发调用多个外部 API(虚拟线程版本,同步写法)
     */
    public List<String> fetchMultipleApis(List<String> urls) {
        return urls.parallelStream()
                .map(url -> {
                    try {
                        HttpRequest request = HttpRequest.newBuilder()
                                .uri(URI.create(url))
                                .timeout(Duration.ofSeconds(5))
                                .GET()
                                .build();
                        return httpClient.send(request, HttpResponse.BodyHandlers.ofString())
                                .body();
                    } catch (Exception e) {
                        return "ERROR: " + e.getMessage();
                    }
                })
                .collect(Collectors.toList());
    }
}

5.2 RestClient(Spring Boot 3.2 新 API)+ 虚拟线程

@Configuration
public class RestClientConfig {

    @Bean
    public RestClient restClient(RestClient.Builder builder) {
        return builder
                .baseUrl("https://api.example.com")
                .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
                .build();
    }
}

@Service
public class ProductService {

    @Autowired
    private RestClient restClient;

    /**
     * 在虚拟线程中同步调用,简洁优雅
     */
    public Product getProduct(Long id) {
        return restClient.get()
                .uri("/products/{id}", id)
                .retrieve()
                .body(Product.class); // 虚拟线程阻塞等待 I/O,不占用 OS 线程
    }

    /**
     * 并发获取多个商品
     */
    public List<Product> getProducts(List<Long> ids) {
        try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
            List<Future<Product>> futures = ids.stream()
                    .map(id -> executor.submit(() -> getProduct(id)))
                    .toList();

            return futures.stream()
                    .map(future -> {
                        try {
                            return future.get(10, TimeUnit.SECONDS);
                        } catch (Exception e) {
                            throw new RuntimeException(e);
                        }
                    })
                    .toList();
        }
    }
}

六、实战:@Async 异步任务升级

6.1 传统 @Async 的局限

// ❌ 传统方式:依赖固定线程池,高并发时线程耗尽
@Configuration
@EnableAsync
public class AsyncConfig {
    @Bean
    public Executor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(20);
        executor.setMaxPoolSize(100); // 上限 100 线程
        executor.setQueueCapacity(500);
        return executor;
    }
}

6.2 虚拟线程版 @Async

// ✅ 虚拟线程方式:无上限并发,低资源占用
@Configuration
@EnableAsync
public class VirtualThreadAsyncConfig {

    @Bean(name = AsyncExecutionAspectSupport.DEFAULT_TASK_EXECUTOR_BEAN_NAME)
    public Executor asyncExecutor() {
        return Executors.newVirtualThreadPerTaskExecutor();
    }
}

@Service
public class NotificationService {

    @Async // 自动使用虚拟线程执行器
    public CompletableFuture<Void> sendEmailAsync(String to, String content) {
        // 模拟发送邮件(I/O 阻塞操作)
        log.info("Sending email to {} on thread: {}, virtual: {}",
                to,
                Thread.currentThread().getName(),
                Thread.currentThread().isVirtual()); // true
        Thread.sleep(200); // 虚拟线程等待,不阻塞 OS 线程
        return CompletableFuture.completedFuture(null);
    }

    @Async
    public CompletableFuture<Void> sendBatchNotifications(List<String> recipients) {
        recipients.forEach(r -> sendEmailAsync(r, "Hello!"));
        return CompletableFuture.completedFuture(null);
    }
}

七、虚拟线程 vs 传统线程池 vs WebFlux 对比

7.1 性能对比(I/O 密集型场景)

方案吞吐量(req/s)内存占用代码复杂度
平台线程池(200线程)~2,000200MB+
虚拟线程~50,000~50MB
WebFlux(响应式)~60,000~40MB
WebFlux + 虚拟线程~65,000~45MB

测试环境:8核16G,模拟每个请求 100ms I/O 延迟,1000 并发用户

7.2 选型建议

我的服务是 I/O 密集型(数据库查询、HTTP调用、文件读写)?
├── 是 ──► 虚拟线程 ✅(简单)或 WebFlux(极致性能)
└── 否(CPU密集型:加解密、图像处理、计算)──► 平台线程池 ✅

我的团队熟悉响应式编程?
├── 是 ──► WebFlux 性能略优,但虚拟线程维护更简单
└── 否 ──► 虚拟线程 ✅(同步代码,零学习成本)

现有项目要改造?
├── Spring MVC 项目 ──► 加一行配置,虚拟线程 ✅ 零改造
└── WebFlux 项目 ──► 继续用,无需切换

八、性能监控与调优

8.1 JDK Flight Recorder 监控虚拟线程

# 启动时开启 JFR 录制
java -XX:StartFlightRecording=duration=60s,filename=vthread.jfr \
     -Djdk.tracePinnedThreads=full \  # 输出 pinning 警告
     -jar app.jar

# 分析 JFR 文件(使用 JDK Mission Control)
jmc vthread.jfr

8.2 检测 Pinning 问题

# JVM 参数:打印 pinned 虚拟线程的堆栈
-Djdk.tracePinnedThreads=full    # 完整堆栈
-Djdk.tracePinnedThreads=short   # 简短日志

启动后,如果存在 pinning 问题,控制台会输出:

Thread[#28,ForkJoinPool-1-worker-1,5,CarrierThreads]
    java.base/java.lang.VirtualThread$VThreadContinuation.onPinned(VirtualThread.java:185)
    com.example.service.UserService.queryUser(UserService.java:42)  ← 问题代码位置

8.3 Micrometer 指标监控

@Configuration
public class VirtualThreadMetricsConfig {

    @Bean
    public MeterBinder virtualThreadMetrics() {
        return registry -> {
            // 自定义虚拟线程计数器
            AtomicLong virtualThreadCount = new AtomicLong(0);
            Gauge.builder("jvm.virtual.threads.active", virtualThreadCount, AtomicLong::get)
                    .description("Active virtual thread count")
                    .register(registry);
        };
    }
}
# 暴露 Actuator 端点
management:
  endpoints:
    web:
      exposure:
        include: health,metrics,prometheus
  metrics:
    tags:
      application: ${spring.application.name}

九、常见坑与最佳实践

9.1 禁止对虚拟线程使用对象池

// ❌ 错误:虚拟线程廉价,不需要池化
ExecutorService pool = new ThreadPoolExecutor(10, 100, ...);

// ✅ 正确:每个任务一个虚拟线程
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();

9.2 谨慎使用 ThreadLocal

// ⚠️ 风险:虚拟线程数量庞大,ThreadLocal 导致内存泄漏
private static final ThreadLocal<Connection> connectionHolder = new ThreadLocal<>();

// ✅ 替代方案:Java 20+ ScopedValue(JEP 429,Java 21 预览)
static final ScopedValue<Connection> CONNECTION = ScopedValue.newInstance();

ScopedValue.where(CONNECTION, getConnection())
        .run(() -> {
            // 在此作用域内安全访问
            Connection conn = CONNECTION.get();
        });

9.3 避免在虚拟线程中使用 synchronized

// ❌ 导致 pinning,性能退化
synchronized (this) {
    jdbcTemplate.query(...); // I/O 阻塞 + pinned = 平台线程性能
}

// ✅ 替换为 ReentrantLock
private final ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
    jdbcTemplate.query(...);
} finally {
    lock.unlock();
}

9.4 最佳实践总结

实践建议
线程命名Thread.ofVirtual().name("vt-task-", 0).factory() 方便调试
异常处理虚拟线程异常不会自动打印,需显式设置 Thread.UncaughtExceptionHandler
CPU 密集型使用独立的平台线程池,避免占用 ForkJoinPool 载体线程
数据库连接池适当增大连接池大小,避免连接成为瓶颈
日志 MDCMDC 基于 ThreadLocal,虚拟线程下需在任务开始时显式复制 MDC

十、完整项目示例

10.1 MDC 在虚拟线程中的传递

@Component
public class VirtualThreadMDCTaskDecorator implements TaskDecorator {

    @Override
    public Runnable decorate(Runnable runnable) {
        // 捕获父线程的 MDC 上下文
        Map<String, String> mdcContext = MDC.getCopyOfContextMap();
        return () -> {
            try {
                if (mdcContext != null) {
                    MDC.setContextMap(mdcContext); // 传递到虚拟线程
                }
                runnable.run();
            } finally {
                MDC.clear();
            }
        };
    }
}

@Configuration
@EnableAsync
public class AsyncConfig {

    @Autowired
    private VirtualThreadMDCTaskDecorator decorator;

    @Bean
    public Executor asyncExecutor() {
        var factory = Thread.ofVirtual()
                .name("async-vt-", 0)
                .factory();
        // 包装为支持 MDC 的执行器
        return new TaskExecutorAdapter(task ->
                Thread.ofVirtual().start(decorator.decorate(task)));
    }
}

10.2 完整 Controller 示例

@RestController
@RequestMapping("/api/v1/orders")
@RequiredArgsConstructor
@Slf4j
public class OrderController {

    private final OrderService orderService;

    /**
     * 并发查询订单详情(聚合多个微服务数据)
     * 虚拟线程让同步代码达到异步性能
     */
    @GetMapping("/{orderId}/detail")
    public OrderDetailVO getOrderDetail(@PathVariable Long orderId) {
        log.info("处理请求,线程: {}, 虚拟线程: {}",
                Thread.currentThread().getName(),
                Thread.currentThread().isVirtual());

        // 并发获取:订单基本信息 + 商品详情 + 用户信息 + 物流状态
        try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
            Future<Order> orderFuture = executor.submit(() -> orderService.getOrder(orderId));
            Future<List<Product>> productsFuture = executor.submit(() -> orderService.getProducts(orderId));
            Future<UserInfo> userFuture = executor.submit(() -> orderService.getUserInfo(orderId));
            Future<LogisticsInfo> logisticsFuture = executor.submit(() -> orderService.getLogistics(orderId));

            // 等待所有结果(虚拟线程阻塞,不占用 OS 线程)
            return OrderDetailVO.builder()
                    .order(orderFuture.get(5, TimeUnit.SECONDS))
                    .products(productsFuture.get(5, TimeUnit.SECONDS))
                    .user(userFuture.get(5, TimeUnit.SECONDS))
                    .logistics(logisticsFuture.get(5, TimeUnit.SECONDS))
                    .build();
        } catch (Exception e) {
            throw new BusinessException("获取订单详情失败", e);
        }
    }
}

十一、总结

Java 21 虚拟线程是 高并发编程的范式革命

维度传统方案虚拟线程
并发模型线程池(有上限)无限轻量级线程
代码风格异步回调 / 响应式同步顺序代码
学习成本高(WebFlux)低(与传统线程一致)
I/O 性能受线程数限制接近响应式
迁移成本极低(一行配置)

适用场景

强烈推荐

  • REST API 服务(大量 HTTP I/O)
  • 数据库查询密集型服务
  • 微服务间 RPC 调用
  • 批量数据处理(文件、消息队列消费)

⚠️ 谨慎使用

  • CPU 密集型计算(加解密、编码转换)
  • 严重依赖 synchronized 的遗留代码(需先解决 pinning)

Spring Boot 3.2+ 一行配置(spring.threads.virtual.enabled=true)即可享受虚拟线程的性能红利,是目前提升 Java 微服务吞吐量最低成本的方案,强烈推荐在生产环境中采用。


💬 互动:你的项目中有没有遇到线程池耗尽导致的性能瓶颈?欢迎在评论区分享你的经验!

🔗 参考资料

0

评论区