Skip to content

虚拟线程 ​

S6-006选修高级约 45 分钟JDK 21Java 高级与架构 · 并发编程

1. 本节定位 ​

并发的终极形态:海量阻塞 IO 不再需要池化。

★ 本节实测环境为 JDK 25(虚拟线程自 JDK 21 正式可用,JEP 444;基线差异说明见 《JDK 与 Java 版本》):

text
10 万个虚拟线程(各 sleep 200ms)→ 1166 ms 全部完成 ✓✓✓
平台线程:10 万个 × 约 1MB 栈 ≈ 100GB 内存 → 不可能 ✗✗

2. 学习目标 ​

  • 理解虚拟线程与平台线程的关系(载体线程 + 挂起)★★★;
  • 知道适用场景(阻塞 IO)与不适用(CPU 密集)★★★;
  • 知道「一个请求一个虚拟线程,不再池化」★★★;
  • 知道 pinning(钉住)问题 ★★。

3. 前置知识检查 ​

4. 为什么需要这个知识 ​

★ 平台线程的天花板 ✗✗(项目 B 实测过 200 并发的上限):

每个平台线程占约 1MB 栈 + 内核资源 → 线程数 = 内存数 ✗

而 IO 密集服务里,线程 99% 时间在等 IO ✗✗

虚拟线程把「等待」变成「挂起」——等的时候不占载体线程 ✓✓✓

5. 核心概念 ​

5.1 原理:载体线程与挂起 ★★★ ​

text
虚拟线程(海量,便宜)
    └─ 挂在「载体线程」(少量平台线程,= CPU 核数)上运行
阻塞时:虚拟线程「卸载」,载体线程去跑别的虚拟线程 ✓✓✓

★ 类比:平台线程是「厨师」,虚拟线程是「订单」—— 阻塞 IO 相当于「等外卖」:BIO 模型下一个厨师等一单到底 ✗✗; 虚拟线程让厨师在等待时去做别的单 ✓✓

5.2 实测结论 ★★★ ​

平台线程虚拟线程
10 万个内存 ≈ 100GB ✗✗1166 ms 完成 ✓✓✓
创建成本重(MB 级栈)轻(KB 级,堆上对象) ✓
阻塞代价占一个线程 ✗挂起,释放载体 ✓✓
需要池化是(池化复用)不需要(用完即弃)✓✓✓

5.3 适用与不适用 ★★★ ​

场景结论
HTTP 客户端调用、JDBC 查询、文件读写 ★★★✓✓✓ 最佳(阻塞 IO)
CPU 密集计算 ✗✗无收益(虚拟线程不增加 CPU)——用线程池 = 核数 ✓
秒杀扣库存(项目 B)✗瓶颈是串行资源,虚拟线程反而放大并发打向数据库 ✗✗

★★★ 一句话:虚拟线程解决「线程不够用」,不解决「资源不够用」 ✓✓✓

5.4 pinning(钉住)★★★ ​

text
虚拟线程在 synchronized 块内阻塞时,会「钉住」载体线程不释放 ✗
→ 载体被占满,其他虚拟线程饿死 ✗✗

JDK 24 已大幅缓解(synchronized 内阻塞不再永久钉住)✓; 但 JDBC 驱动等本机调用仍可能钉住 ⚠️ —— 监控:-Djdk.tracePinnedThreads=full ✓✓

★ 迁移注意 ✓:ThreadLocal 大量使用的服务迁移虚拟线程要小心(百万虚拟线程 × 大 ThreadLocal = 内存爆炸)✗✓

5.5 使用方式 ★★ ​

java
// ① 直接起
Thread.ofVirtual().start(() -> handle(request));

// ② 执行器(Spring Boot 3.2+ 一行配置)
spring.threads.virtual.enabled=true
// Tomcat 改为每请求一个虚拟线程 ✓✓✓

★ 与项目 B 的关系 ✓✓✓:

项目 B 用 200 平台线程池 → 并发上限 200 ✗

虚拟线程 → 并发上限 ≈ 内存/文件句柄(几十万)✓✓✓

但数据库连接池不会变 ✗ ——瓶颈只是后移(先连接池后数据库)✓✓ (呼应 《项目 B:性能测试与故障分析》 的「先坏清单」✓)

6. 递进示例 ​

6.1 最小示例 ​

java
Thread.ofVirtual().name("io-", 0).start(() -> callRemote());

6.2 业务示例:Spring Boot 启用 ​

properties
spring.threads.virtual.enabled=true
# 现有代码不变,Tomcat 每请求一个虚拟线程 ✓✓✓

6.3 反例:虚拟线程里跑 CPU 密集 ​

java
Thread.ofVirtual().start(() -> encryptBigFile());   // ✗ 无收益还有调度开销

7. 完整可运行代码与实测 ​

代码:labs/java-deep/src/main/java/cn/kb/learn/deep/concurrency/JucVirtualThreadLesson.java

实测输出:

text
== 3. 100k virtual threads, each sleeping 200ms ==
  done in 1166 ms

8. Debug 观察点 ​

  1. 把 10 万改成 100 万,观察内存与时间 ✓;
  2. 虚拟线程里做 1 秒 CPU 计算 ×10 万:观察吞吐不升反降 ✗✗;
  3. -Djdk.tracePinnedThreads=full 找钉住点 ✓。

9. 常见错误 ​

#错误说明
1CPU 密集任务用虚拟线程 ✗✗无收益 ✓✓
2以为虚拟线程能省数据库连接 ✗✗连接池是另一个瓶颈 ✓✓
3给虚拟线程配池 ✗✗反模式(设计目标就是不用池)✓✓
4ThreadLocal 塞大对象 ✗✗百万级放大 ✓
5synchronized 内长时间阻塞 ⚠️可能钉住载体(旧 JDK)✓

10. 动手实验 ​

  1. 跑 10 万虚拟线程实验 ✓✓;
  2. 对比:200 平台线程 vs 10 万虚拟线程完成同批 sleep 任务 ✓✓✓;
  3. 给 Spring Boot 开 virtual.enabled=true,压测对比 ✓✓。

验收方式:能说出「你的服务哪些环节适合迁移虚拟线程、哪些不适合」。

扩展任务:把项目 A 的借书接口改成虚拟线程版并压测对比 ✓✓

11. 练习题 ​

  1. 虚拟线程解决什么问题?
  2. 虚拟线程需要池化吗?
  3. CPU 密集任务该用什么?
  4. pinning 是什么?
  5. 虚拟线程能提升数据库并发吗?
参考答案(要点)
  1. 海量阻塞 IO 的并发(把阻塞从「占线程」变成「挂起」)✓✓★
  2. 不需要 ✗✓ ——用完即弃,池化是反模式 ✓✓★
  3. 线程池 = CPU 核数(虚拟线程无收益)✓★
  4. 虚拟线程在 synchronized/本机调用内阻塞时无法卸载,占住载体线程 ✗ —— 用 -Djdk.tracePinnedThreads 定位 ✓★
  5. 不能 ✗ ——连接池与数据库容量才是瓶颈(虚拟线程只会放大打向库的并发)✗✓, 需要配合信号量限流(每个连接一个许可) ✓✓★

12. 掌握标准 ​

  • [ ] 理解载体线程与挂起 ★★★;
  • [ ] 知道适用(阻塞 IO)与不适用(CPU)★★★;
  • [ ] 知道不用池化 ★★★;
  • [ ] 知道 pinning 与监控 ★★;
  • [ ] 知道「瓶颈只是后移」★ ★★★。

13. 扩展资源 ​

14. 下一步学习 ​

《JVM 内存结构与对象创建》。

后续知识

暂无

进度只保存在本机浏览器,可随时清除

个人学习项目:AI 生成内容均经人工复核,第三方资源仅作索引