09.2 - 虚拟线程原理
定位: M:N 线程模型——让百万级并发成为现实,Java 的"协程"时刻 面试高频度: ⭐⭐⭐⭐⭐
一、这是什么?为什么需要它?
传统(平台)线程的问题
java
// 传统平台线程 = 1:1 映射到 OS 线程
Thread t = new Thread(() -> process(request));
t.start(); // OS 线程创建 → 分配 1MB 栈
t.join(); // OS 线程阻塞 → 上下文切换 (~1μs)代价:
| 资源 | 平台线程 | 说明 |
|---|---|---|
| 栈空间 | ~1MB 固定 | 100 万线程 = 1TB 内存 |
| 创建时间 | ~1ms | OS 系统调用 |
| 上下文切换 | ~1μs | OS 内核态切换 |
| 最大并发 | 数千 | 受限于 OS 线程数 |
结论:无法用平台线程做高并发 I/O。传统 Servlet 容器(Tomcat)用线程池(通常 200-500)限制并发。
虚拟线程的核心洞察
"大部分线程大部分时间在阻塞"
等待数据库查询、等待下游 API 响应、等待文件 I/O——这些阻塞操作不消耗 CPU,却占着宝贵的 OS 线程。
如果阻塞时释放 OS 线程,OS 线程数就可以远少于并发任务数。这就是虚拟线程的魔力:M:N 映射。
二、原理拆解
2.1 M:N 线程模型
传统 1:1 模型:
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ 平台线程 T1 │ │ 平台线程 T2 │ │ 平台线程 T3 │
│ (OS 线程 1) │ │ (OS 线程 2) │ │ (OS 线程 3) │
└──────────────┘ └──────────────┘ └──────────────┘
虚拟线程 M:N 模型:
┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ 虚拟线程 V1 │ │ 虚拟线程 V2 │ │ 虚拟线程 V3 │ │ 虚拟线程 V4 │
│ (运行中) │ │ (阻塞→挂起) │ │ (运行中) │ │ (就绪) │
└──────┬───────┘ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘
│ │ │ │
▼ ▼ ▼ ▼
┌──────────────────────────────────────────────────────────────┐
│ ForkJoinPool (载波线程池) │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ 载波 C1 │ │ 载波 C2 │ │ 载波 C3 │ │
│ │(平台线程 P1) │ │(平台线程 P2) │ │(平台线程 P3) │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
└──────────────────────────────────────────────────────────────┘
│ 1:1
▼
┌──────────────────────┐
│ OS 线程 (载体) │
│ P1 P2 P3 │
│ (通常 = CPU 核数) │
└──────────────────────┘2.2 挂起与恢复(Continuation)
虚拟线程的挂起是用户态的——不涉及 OS 内核:
虚拟线程执行:
┌──────────────────────────────────────────────────┐
│ public void handle() { │
│ String data = db.query(sql); ← 阻塞! │
│ // 恢复后从这里继续 │
│ process(data); │
│ } │
└──────────────────────────────────────────────────┘
│ 遇到阻塞操作 (如 socket.read())
▼
┌──────────────────────────────────────────────────┐
│ 1. JVM 捕获当前执行状态 (Continuation) │
│ - 程序计数器 (PC): db.query() 之后 │
│ - 栈帧: handle() 中的局部变量 │
│ - 将栈帧复制到堆 (而非 OS 栈) │
│ │
│ 2. 释放载波线程 │
│ - 载波线程回到 ForkJoinPool │
│ - 可以运行另一个虚拟线程 │
│ │
│ 3. I/O 完成时 (回调) │
│ - 虚拟线程被标记为 RUNNABLE │
│ - JVM 分配给某个可用的载波线程 │
│ - 从堆中恢复 Continuation │
│ - 从 PC 指向的位置继续执行 │
└──────────────────────────────────────────────────┘关键:虚拟线程的"栈"存在堆上,不是 OS 栈。挂起 = 把 Java 栈帧拷贝到堆对象里。恢复 = 把堆对象拷回到 Java 栈。不涉及 OS 内核调用,延迟是微秒级的。
2.3 Pinning("钉住"——最大的坑)
java
// ⚠️ Pinning 场景 1: synchronized 块
synchronized (lock) { // 虚拟线程"钉"在载波上
db.query(sql); // 阻塞→载波也被阻塞!
}
// ✅ 正确: 用 ReentrantLock 代替
lock.lock();
try {
db.query(sql); // 阻塞→载波释放→另一个虚拟线程用
} finally {
lock.unlock();
}
// ⚠️ Pinning 场景 2: native 方法 / JNI
// 调用 native 方法时,载波线程无法释放为什么 synchronized 会导致 pinning?
synchronized 的监视器关联的是当前线程(OS 线程)。如果虚拟线程在 synchronized 块中被挂起,然后恢复时被分配到一个不同的载波线程——监视器的所有者就"不对"了。
JVM 团队正在解决这个问题("synchronized 适应虚拟线程"),但在完全修复前,虚拟线程重的代码建议用 ReentrantLock。
2.4 性能对比
| 维度 | 平台线程 | 虚拟线程 | 差距 |
|---|---|---|---|
| 最大并发数 | ~几千 | 数百万 | 1000x |
| 线程创建耗时 | ~1ms | ~1μs | 1000x |
| 上下文切换 | ~1μs(OS) | ~100ns(JVM) | 10x |
| 栈占用 | ~1MB | ~1KB(无 stack + 堆存储) | 1000x |
| CPU Bound | 无差异 | 略有额外开销 | 同等 |
| I/O Bound | 线程池限制 | 无限制 | 极优 |
三、图解全景
虚拟线程生命周期:
┌──────────────────────────────────────────────────┐
│ 虚拟线程状态机 │
│ │
│ Created │
│ │ start() │
│ ▼ │
│ RUNNABLE ──→ Carrier 线程执行 │
│ │ │
│ ├── I/O 阻塞 ──→ PARKING │
│ │ ├─ 保存 Continuation → 堆 │
│ │ ├─ 释放 Carrier │
│ │ └─ I/O 完成 → 重新调度 │
│ │ │
│ ├── synchronized ──→ PINNED (载波不释放!) │
│ │ │
│ └── native 方法 ──→ PINNED │
│ │
│ └── 执行完毕 → TERMINATED │
└──────────────────────────────────────────────────┘
线程转储 (jstack) 示例:
"虚拟任务 #1234" virtual
└─ parked: 等待数据库查询 ← 虚拟线程挂起
└─ carrier: "ForkJoinPool-1-worker-3" ← 它在哪个载波上运行四、面试视角
| 面试题 | 核心回答 |
|---|---|
| 虚拟线程和平台线程的区别? | 平台线程 1:1 映射 OS 线程(大栈、贵、有限),虚拟线程 M:N 映射(小栈、轻量、百万级)。虚拟线程的挂起/恢复在用户态完成 |
| 虚拟线程的实现原理? | Continuation(JVM 保存栈帧到堆) + M:N 调度(ForkJoinPool)+ 阻塞操作时挂起、恢复时重建栈帧 |
| 什么是 pinning? | 虚拟线程在 synchronized 块或 native 方法中时,"钉住"载波线程无法释放。此时阻塞会导致载波也阻塞,降低并发能力 |
| 为什么 synchronized 会导致 pinning? | synchronized 监视器绑定 OS 线程。虚拟线程恢复时可能分配不同的载波,监视器所有权会混乱。Loom 团队正在解决 |
| 虚拟线程能替代 Reactive 编程? | 能大幅简化。传统响应式(WebFlux、RxJava)本质是解决 OS 线程不够用的问题。虚拟线程让"每个任务一个线程"重新可行 |
| 什么场景不适合虚拟线程? | CPU 密集型(数学计算、加密解密)——不阻塞,虚拟线程无优势。锁竞争激烈——多虚拟线程争锁 → pinning + 载波抢占 |
追问预判
问: 虚拟线程在性能测试中表现如何?
- 答: I/O 密集型场景(网关、代理、数据库查询)虚拟线程可将吞吐量提升 5-10x。但 CPU-bound 场景无优势,且线程切换开销仍在
问: Spring Boot 3.x 中的虚拟线程支持?
- 答: Spring Boot 3.2+ 内置支持,
spring.threads.virtual.enabled=true。Tomcat 会用虚拟线程处理请求,每个请求一个虚拟线程
- 答: Spring Boot 3.2+ 内置支持,
问: 虚拟线程的调试和监控有变化吗?
- 答: jstack 可以同时显示虚拟线程和平台线程。JFR 支持虚拟线程事件。IDEA 2023.3+ 支持虚拟线程调试。注意:Java Flight Recorder 可以给出虚拟线程的挂起原因
📚 相关链接
- **Java虚拟机栈** — 虚拟线程的"栈"与平台线程栈的区别
- **JMM与happens-before** — JMM 在虚拟线程下同样适用
- **ZGC原理** — 另一个全并发特性
- **OOM排查方法论** — 虚拟线程过多可能导致堆 OOM(栈帧在堆上)
- ← 返回 **前沿索引**