原创

高并发网络编程(四):JDK 21 虚拟线程

温馨提示:
本文最后更新于 2026年03月22日,已超过 6 天没有更新。若文章内的图片失效(无法正常加载),请留言反馈或直接联系我

 

针对 JDK 21 虚拟线程(Virtual Threads),这是 Java 并发编程历史上最具颠覆性的更新之一。结合最新的行业实践(2025-2026年),以下是关于它的核心要点、使用方式、性能表现及避坑指南。

一、核心概念

虚拟线程是 JVM 管理的轻量级用户态线程,实现了 Project Loom 的核心愿景。

  • 传统线程(平台线程):java.lang.Thread 直接映射到操作系统内核线程(1:1 模型)。创建开销大,栈内存固定(通常 1MB+),系统能承载的线程数有限(几千到几万)。
  • 虚拟线程:由 JVM 调度,多路复用到少量的平台线程(载体线程,Carrier Threads)上运行(M:N 模型)。
    • 极轻:初始栈内存仅约 1-2 KB,可动态伸缩。
    • 极多:单台机器可轻松创建 数百万甚至上千万 个虚拟线程。
    • 非阻塞:当虚拟线程执行阻塞操作(如 I/O、Thread.sleep)时,JVM 会自动将其“卸载”(unmount),释放底层的载体线程去执行其他任务,待阻塞结束后再“挂载”回去。

二、如何使用

在 JDK 21 中,使用虚拟线程非常简单,无需引入任何第三方库。

方式一:直接启动

// 启动一个虚拟线程
Thread.startVirtualThread(() -> {
    System.out.println("运行在虚拟线程: " + Thread.currentThread());
    // 模拟阻塞操作,不会阻塞真正的操作系统线程
    try { Thread.sleep(1000); } catch (InterruptedException e) {}
});

方式二:使用新的 ExecutorService

这是最推荐的方式,可以替换现有的线程池代码。

try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    IntStream.range(0, 10_000).forEach(i -> {
        executor.submit(() -> {
            // 每个任务都是一个独立的虚拟线程
            performIoTask(); 
            return i;
        });
    });
} // try-with-resources 会等待所有任务完成

方式三:Spring Boot 3.2+ 集成

如果你使用 Spring Boot 3.2 或更高版本,只需在配置文件中开启即可让 Tomcat/Jetty 使用虚拟线程处理请求:

# application.properties
spring.threads.virtual.enabled=true

这样,每个 HTTP 请求都会在一个独立的虚拟线程中处理,无需修改业务代码。

三、性能与优势

根据 2025-2026 年的生产环境数据,虚拟线程的表现如下:

特性

传统平台线程

虚拟线程 (JDK 21)

优势解读

并发量级

数千 ~ 数万

数百万 ~ 上千万

轻松应对高并发流量,不再需要复杂的异步回调。

内存占用

高 (每线程 ~1MB+)

极低 (每线程 ~1-2KB)

大幅降低内存压力,减少 OOM 风险。

编程模型

同步阻塞 或 复杂异步

同步阻塞

“写同步代码,获异步性能”。代码可读性、调试难度大幅降低。

吞吐量 (I/O)

基准

提升 10 倍 ~ 100 倍

在高延迟 I/O 场景(如调用多个微服务、查库)下,吞吐量呈指数级增长。

上下文切换

昂贵 (内核态)

廉价 (用户态)

JVM 内部调度,效率极高。

典型场景增益:

  • 对于一个需要串行调用 3 个外部 API 和 1 次数据库查询的接口,在同等硬件资源下,QPS 可从传统的 2,000 提升至 20,000+

四、最佳实践与避坑

结论:强烈推荐用于新项目和高并发 I/O 场景,但需注意适用边界。

1.强烈推荐场景

  1. I/O 密集型应用:Web 服务器、网关、微服务调用、数据库访问、文件读写等。这是虚拟线程的主战场。
  2. 简化异步代码:想要摆脱 CompletableFuture、Reactor (WebFlux) 等复杂异步框架带来的“回调地狱”和调试困难,回归直观的同步编程风格。
  3. 高并发连接:需要维持大量空闲连接或长轮询(Long Polling)的场景(如即时通讯、推送服务)。

2.慎用/不推荐场景

  1. CPU 密集型任务
    • 如:图像渲染、加密解密、大规模数值计算。
    • 原因:虚拟线程的优势在于“阻塞时让出 CPU”。如果任务一直占用 CPU 不阻塞,虚拟线程不仅没有优势,反而因为调度开销略低于平台线程。此类任务请继续使用固定大小的平台线程池 (Executors.newFixedThreadPool)。
  1. 过度依赖 ThreadLocal:
    • 虽然 JDK 21 优化了 ThreadLocal,但如果每个虚拟线程都存储大量数据且未及时清理,百万级线程仍可能导致内存问题。
    • 建议:优先考虑使用 JDK 21 引入的 Scoped Values(作用域值,目前仍在预览/完善中,但在未来是更优解)来替代 ThreadLocal 传递上下文。
  1. 锁竞争严重且使用 synchronized:
    • 在早期版本中,synchronized 块可能导致载体线程阻塞(Pinning 问题)。虽然 JDK 21 已大幅优化(JIT 编译器会尝试避免),但在极高竞争的临界区,仍建议优先使用 java.util.concurrent.locks.ReentrantLock。
  1. 连接池瓶颈:
    • 虚拟线程能创建百万个,但数据库连接池(如 HikariCP)通常只有几十个连接。如果不调整连接池策略或使用多路复用技术,数据库会成为新的瓶颈,导致大量虚拟线程排队等待连接,失去意义。
正文到此结束