系列:《Java Virtual Threads(虚拟线程)教程:从零上手到实战优化》
第 3 篇:虚拟线程调度机制深度解析:平台线程、纤程、栈帧、挂起与恢复
本篇是整个系列中最重要的底层文章之一。
如果你理解了 Virtual Threads 的运行机制,你就能完全理解为什么“同步写法 = 异步性能”。
目录
虚拟线程究竟由谁执行?(平台线程 vs 虚拟线程)
调度器是怎样工作的?(ForkJoinPool 的新模式)
虚拟线程阻塞时到底发生了什么?(挂起、保存栈帧)
栈管理:为什么虚拟线程只有几十 KB?
Pinning:唯一会让虚拟线程变慢的情况
为什么虚拟线程不会提升 CPU 性能?
执行流程:从提交任务到执行完成的全生命周期图
本篇总结
一、虚拟线程究竟由谁执行?
这是理解 Virtual Threads 的关键。
♦ 传统线程模型
Java Thread 1 ----> OS Thread 1
Java Thread 2 ----> OS Thread 2
Java Thread N ----> OS Thread N
一个 Java Thread 就对应一个 OS Thread,资源消耗大、切换开销高。
♦ 虚拟线程模型(Fiber 模型)
Virtual Threads(100000+)
↓(调度)
Platform Threads(几十个)
↓
CPU 核心
虚拟线程由少量“平台线程”执行。
平台线程就是 OS Thread,但数量极少,比如:
8 核 CPU → 可能只有 16~32 个平台线程
虚拟线程只是运行在平台线程上的一个“任务”,它的状态由 JVM 管理。
二、虚拟线程调度器到底是谁?
虚拟线程调度器由 Java 内置的 ForkJoinPool(新模式) 提供,但不是经典的 Fork/Join 线程池。
区别:
虚拟线程的调度过程非常简单:
任务来了 → 派给一个平台线程执行 → 遇到阻塞?挂起 → 派给下一个虚拟线程
用最小的线程数,执行最多的任务。
三、虚拟线程阻塞时到底发生了什么?(关键知识点)
这是理解性能提升的核心 —— “阻塞是廉价的”。
假设有一个虚拟线程:
Thread.sleep(2000);
传统线程的行为:
Java Thread/OS Thread 都“睡着”
占用内核资源,不可调度
影响吞吐量
虚拟线程的行为:
虚拟线程执行到 sleep → JVM 检测到阻塞调用
虚拟线程被挂起(park)
此时该虚拟线程的栈帧会被保存到 heap 或结构化栈中
占用该平台线程的执行权被释放
平台线程马上去执行其他虚拟线程(几乎无成本)
虚拟线程(挂起)
平台线程(继续工作)
结论:阻塞不再占用平台线程。因此同步代码也能达到异步的吞吐性能。
四、虚拟线程的栈是如何管理的?为什么它如此轻量?
平台线程的栈是:
固定大小(一般 1MB)
OS 分配
内存浪费严重
虚拟线程的栈则完全不同:
♦ 特性:栈是“动态增长 + 分段存储”
虚拟线程使用 栈片(Stack Chunk) 技术:
每个 chunk 只有几 KB
栈多了再增加 chunk(链式结构)
阻塞时只需要保存当前活跃栈片
[Chunk 1] → [Chunk 2] → [Chunk 3] ...
因此:
空闲虚拟线程消耗极低
可轻松创建百万级线程
栈帧保存和恢复超快
五、Pinning:唯一可能让虚拟线程变慢的情况(重要)
Pinning = 假设虚拟线程正在执行某个必须绑定到平台线程的代码段,这时它就无法挂起。
虚拟线程会“钉在平台线程上”,无法释放。
哪些情况会发生 Pinning?
1. 使用 synchronized
synchronized(obj) {
Thread.sleep(1000); // pinning
}
因为 synchronized 依赖平台线程的 monitor。
2. native 方法阻塞
如:
File I/O(旧版)
socket 调用(部分场景)
system calls
3. 传统 Blocking I/O(如 FileInputStream.read())
JDK 21 已优化大部分网络 I/O,但本地文件可能仍导致 pinning。
Pinning 的影响
虚拟线程被固定到某个平台线程
阻塞期间平台线程无法执行其他虚拟线程
吞吐性能下降
如何避免?
使用
java.util.concurrent的锁,而不是 synchronized使用 Java NIO 或JDK21+ 的优化 I/O(大部分已解决)
避免 native 阻塞
六、为什么虚拟线程不能提升 CPU 性能?
虚拟线程适合 I/O 密集型,不适合 CPU 密集型。
原因:
CPU 密集型任务不会阻塞
虚拟线程无法“挂起”
最终执行速度仍然取决于 CPU 核心数
如果你跑矩阵乘法、加密计算等 CPU-heavy 任务:
虚拟线程 ≈ 传统线程
最多跑满 CPU 核心,不可能更快
七、虚拟线程完整生命周期(流程图)
创建 Virtual Thread
│
▼
提交到调度器(Scheduler)
│
▼
分配给平台线程执行
│
▼
┌─────────────阻塞调用─────────────┐
│ │
▼ ▼
是否可安全挂起? Pinning(不可挂起)
│ │
是 ▼ ▼ 否
保存栈帧 持续占用平台线程
释放平台线程 性能下降
│
▼
等待恢复
│
▼
恢复栈帧 → 重新派发到平台线程 → 继续执行
│
▼
结束
八、本篇总结
通过本篇,你应该已经理解:
✔ 虚拟线程运行在少量平台线程上
✔ 阻塞时虚拟线程会挂起,不占用平台线程
✔ 栈以“分段方式”保存,因此虚拟线程极轻
✔ 唯一性能陷阱是 Pinning(比如 synchronized)
✔ 虚拟线程适合 I/O 密集,不是 CPU 密集
✔ 调度器本质是 ForkJoinPool 的增强模式
这些原理解释了:
为什么 Java 可以在不改变同步写法的前提下,做到异步级别的吞吐性能。
你成为虚拟线程专家的核心基础已经打好。
下一篇预告(第 4 篇)
《Spring Boot 与虚拟线程:线程池、Web 请求、RestTemplate、WebClient 全面迁移指南》
内容将包括:
Spring Boot 如何启用虚拟线程?
如何让 Controller → Service → Repository 全链路使用虚拟线程?
数据库连接池如何配合虚拟线程?
虚拟线程 + Tomcat / Undertow / Netty 的对比
全代码实战:重写一个同步 REST 服务,让性能提升数倍
默认评论
Halo系统提供的评论