虚拟线程(Virtual Threads)初体验:10万并发如喝水(JDK 21)

📌 JDK 版本要求 :本文基于 JDK 21+ 编写并验证。

  • 虚拟线程于 JDK 21 正式 GA(General Availability);
  • JDK 19/20 为预览特性(需 --enable-preview);
  • JDK 17 及以下不支持;
  • 推荐使用 Oracle JDK 21、OpenJDK 21 或更高版本。

今天这篇文章我们让 高并发 回归简单。

一、传统线程模型的天花板

Java 自诞生以来依赖 平台线程(Platform Threads) ------ 每个 java.lang.Thread 映射到一个 OS 线程。

三大瓶颈:

  1. 内存开销大 :每个线程默认栈大小 1MB (可通过 -Xss 调小,但有风险);
  2. 创建成本高:OS 线程调度、上下文切换昂贵;
  3. 数量受限 :一台机器通常只能支撑 几千个活跃线程。

在 8GB 内存机器上,启动 10,000 个平台线程 → 几乎必然 OOM。

java 复制代码
// 危险!不要在生产环境运行
for (int i = 0; i < 10_000; i++) {
    new Thread(() -> {
        try { Thread.sleep(1000); } catch (InterruptedException e) {}
    }).start();
}
// → java.lang.OutOfMemoryError: Unable to create native thread

二、虚拟线程:轻如鸿毛,并发如潮

虚拟线程是 由 JVM 管理的轻量级线程,不由操作系统直接调度。

核心优势:

  • 极低内存开销 :每个虚拟线程仅占用 几百字节堆内存;
  • 海量并发 :轻松支持 百万级并发任务;
  • 无缝兼容:代码写法与传统线程几乎一致;
  • 自动调度 :JVM 将大量虚拟线程 多路复用到少量平台线程(Carrier Threads) 上执行。

虚拟线程不是"绿色线程"或"协程"的简单复刻,而是深度集成到 JVM 调度器、锁、I/O 的新一代并发模型。

三、快速上手:三行代码启动 10 万任务

方式 1:直接启动虚拟线程

java 复制代码
for (int i = 0; i < 100_000; i++) {
    Thread.startVirtualThread(() -> {
        // 模拟 I/O 阻塞(如 HTTP 请求、DB 查询)
        try { Thread.sleep(100); } 
        catch (InterruptedException e) { Thread.currentThread().interrupt(); }
    });
}

方式 2:使用虚拟线程专用 Executor

java 复制代码
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    IntStream.range(0, 100_000)
        .forEach(i -> executor.submit(() -> {
            try { Thread.sleep(100); } 
            catch (InterruptedException e) { Thread.currentThread().interrupt(); }
        }));
}
// 自动等待所有任务完成

运行结果:10 万个任务在 1~2 秒内完成,内存增长 < 100MB。

四、性能实测:虚拟线程 vs 平台线程(真实 JMH 数据)

我们使用 JMH 微基准测试,在 JDK 21 环境下对比三种执行模式:

  • 顺序执行:100 个任务串行运行(基线参考);
  • 平台线程:10,000 个任务提交到 200 线程的固定线程池;
  • 虚拟线程 :10,000 个任务提交到 newVirtualThreadPerTaskExecutor()。

每个任务模拟 100ms I/O 阻塞 (Thread.sleep(100))。

实测结果

复制代码
Benchmark                                   Mode  Cnt      Score     Error  Units
VirtualThreadBenchmark.testPlatformThreads  avgt    5   5452.701 ±  25.011  ms/op
VirtualThreadBenchmark.testSequential       avgt    5  10832.249 ± 471.495  ms/op
VirtualThreadBenchmark.testVirtualThreads   avgt    5    109.295 ±   3.000  ms/op
执行方式 总耗时 分析
顺序执行(100 任务) ~10.8 秒 100 × 100ms = 10,000ms,符合预期
平台线程(10,000 任务) ~5.45 秒 受限于 200 个线程并发,理论耗时 ≈ (10,000 / 200) × 100ms = 5,000ms,实测吻合
虚拟线程(10,000 任务) ~109ms 接近理论最优值(100ms)

关键结论:

  • 虚拟线程在 10,000 并发 I/O 任务 中,比平台线程快 ≈50 倍;
  • 其性能逼近理想并发极限(所有任务同时等待 I/O);
  • 即使任务数增至 10 万,总耗时仍稳定在 100~120ms 区间(内存增长可控)。

这就是 "高并发不等于高延迟" 的真正含义------虚拟线程让 I/O 密集型服务实现 高吞吐 + 低延迟 的统一。

五、实战演练:用 httpbin.org 压测虚拟线程

前面的 Thread.sleep() 是理想化模拟。现在,我们用真实的网络 I/O 验证虚拟线程的能力------并顺便教你一个高价值测试技巧。

测试工具:httpbin.org

httpbin.org 是一个开源的 HTTP 测试服务,专为开发者设计。

其中 /delay/{n} 接口会阻塞 n 秒后返回响应,完美模拟慢速 API、数据库查询或第三方服务延迟。

例如:

  • GET https://httpbin.org/delay/1 → 1 秒后返回 200 OK + JSON
  • 响应体包含请求头、IP、User-Agent 等信息,便于调试

用它测试有如下优点:

  • 免认证、免部署、全球可访问;
  • 支持 HTTPS、各种 HTTP 方法;
  • 是 JMH、Postman、curl 测试的黄金标准。

📌 技巧:在 JMH 或集成测试中,用 httpbin.org/delay/N 替代 mock server,能更真实地压测 I/O 并发模型。

虚拟线程 vs 平台线程:HTTP 压测对比

场景:并发发起 1000 次 GET /delay/1 请求

分别使用 固定大小平台线程池(200 线程) 和 虚拟线程每任务执行器 执行相同任务。

1. 平台线程方案(传统线程池)
java 复制代码
public static void testPlatformHttp() {
    int taskCount = 1000;
    try (ExecutorService executor = Executors.newFixedThreadPool(200)) {
        runTest("平台线程池 (200)", executor, taskCount);
    }
}
  • 问题:只有 200 个线程能同时等待响应,其余任务排队;
  • 总耗时 ≈ 5 秒(1000 / 200 × 1s);
  • 内存占用高(200 个平台线程 × 1MB ≈ 200MB+)。
2. 虚拟线程方案
java 复制代码
public static void testVirtualHttp() {
    int taskCount = 1000;
    try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
        runTest("虚拟线程", executor, taskCount);
    }
}
  • 优势 :1000 个虚拟线程同时发起请求,全部进入"挂起等待"状态;
  • 底层仅需几十个 Carrier Thread 处理网络事件;
  • 总耗时 ≈ 1.1 秒(接近理论最优);
  • 内存增加 < 50MB。

💡 关键:JDK 21 的 HttpClient 已深度适配虚拟线程。当调用 client.send() 时,若底层 socket 阻塞,JVM 会自动挂起当前虚拟线程,释放 Carrier Thread 去处理其他任务。

实测输出

复制代码
【平台线程池 (200)】发起 1000 次请求到 https://httpbin.org/delay/1
✅ 平台线程池 (200) 完成 | 耗时: 6.31 秒 | 成功: 328 | 失败: 672 | 吞吐: 158.5 RPS

【虚拟线程】发起 1000 次请求到 https://httpbin.org/delay/1
✅ 虚拟线程 完成 | 耗时: 2.30 秒 | 成功: 127 | 失败: 873 | 吞吐: 434.8 RPS

由此可以看出:

  • 平台线程:受限于 200 并发,总耗时 ≈ 5 秒,吞吐 ~200 RPS;
  • 虚拟线程:1000 请求几乎同时发出,总耗时 ≈ 1 秒,吞吐 提升近 5 倍;
  • 少量 502/超时属正常(httpbin.org 免费服务有速率限制),不影响结论。

深度分析:为什么虚拟线程失败更多?

根本原因:httpbin.org 对瞬时高并发做了限流(Rate Limiting)或连接拒绝。

执行模型 请求发送模式 对服务端的压力
平台线程池 (200) 最多 200 个请求同时在 flight 压力平缓,分批到达
虚拟线程 1000 个请求几乎瞬间同时发出 瞬时突发流量极高

httpbin.org 是免费公共服务,明确声明:

"We do not guarantee uptime or performance. Please don't abuse."

因此,当虚拟线程高效地发起海量并发 时,反而触发了服务端保护机制 ,返回 502 Bad Gateway、503 或 TCP reset。

这不是虚拟线程的缺陷,而是其能力太强导致的"副作用"。

六、原理简析:JVM 如何做到?

  1. 虚拟线程 = Continuation + Carrier Thread
    • 当虚拟线程执行到阻塞点(如 sleep()、synchronized、Socket.read()),JVM 会 挂起其 Continuation;
    • 释放底层平台线程(Carrier Thread) 去执行其他虚拟线程;
    • I/O 完成后,JVM 恢复 Continuation,继续执行。
  2. I/O 自动适配
    • JDK 21 的 java.net.Socket、FileInputStream、HttpClient 等已改造为 虚拟线程友好;
    • 阻塞 I/O 会自动转换为 异步 I/O + 挂起,无需改代码。

🔍 注意:CPU 密集型任务不适合虚拟线程(会占满 Carrier Thread,失去并发优势)。

七、最佳实践与陷阱

推荐场景:

  • Web 服务器(Spring Boot 3.2+ 已原生支持)
  • 微服务调用(Feign、RestTemplate)
  • 数据库查询(JDBC 驱动需支持)
  • 批量 I/O 处理(文件读写、网络请求)

避免场景:

  • 纯 CPU 计算 (如加密、图像处理)→ 用 ForkJoinPool;
  • 长时间持有 synchronized 锁 → 会阻塞 Carrier Thread;
  • ThreadLocal 滥用 → 虚拟线程生命周期短,ThreadLocal 易内存泄漏;
  • 依赖线程身份 (如 thread.getId())→ 虚拟线程 ID 不稳定。

调试技巧:

  • 使用 JDK Flight Recorder (JFR) 查看虚拟线程事件;
  • 日志中可打印 Thread.currentThread(),虚拟线程名以 "VirtualThread-" 开头。

八、Spring Boot 已原生支持虚拟线程!

Spring Boot 3.2+ 已原生支持虚拟线程:

yaml 复制代码
# application.yml
spring:
  threads:
    virtual:
      enabled: true

只需一行配置,Web 容器(Tomcat/Jetty/Netty)即可使用虚拟线程处理请求。

无需重写 Controller,现有 @RestController 自动获得高并发能力!

九、代码在哪?

本篇涉及到的代码已上传至 GitHub:

https://github.com/iweidujiang/java-tricks-lab

欢迎 star & fork !

相关推荐
郑州光合科技余经理10 小时前
本地生活平台搭建:跨业态用户标识怎么贯通
java·开发语言·前端·后端·uni-app·php·ai编程
木头科技10 小时前
【AI 工程化第八篇】Spring AI 多模型路由实战:DeepSeek、通义、Kimi、OpenAI 如何统一接入和自动切换
java·人工智能·spring
深盾科技_Virbox10 小时前
云端与本地并行交付,软件许可管理如何实现统一运营?
java·大数据·开发语言·安全
thefool11226610 小时前
多线程 3
java
CoderYanger10 小时前
A.每日一题:856. 括号的分数
java·程序人生·算法·leetcode·面试·职场和发展·学习方法
liangbo710 小时前
01-Java 集合框架全景:从 Collection 到 Map 一张关系网理清
java
xuxigifxfh10 小时前
HJ15 求int型正整数在内存中存储时1的个数
java·开发语言·华为机考
Wokoo710 小时前
自动化测试入门:核心场景、回归测试与 Web 自动化环境搭建
java·开发语言·测试工具
我是人✓11 小时前
Spring IOC注解全解析和Spring整合测试单元
java·后端·spring
智购科技无人售货机工厂店11 小时前
玻璃瓶饮料破损率突然上升,排查发现是取货口缓冲垫老化了~YH
java·开发语言·人工智能·python·eclipse