一、先给结论
虚拟线程不是不运行在 OS 线程上,而是:
只在“真正需要 CPU 时”才短暂占用 OS 线程。
在 IO 等待时,JVM 会把它“卸载”下来。
二、为什么传统线程一定占用 OS 线程?
1️⃣ Java 线程 = OS 线程(1:1)
在 Java 21 之前:
JavaThread<==>OSThread当你写:
Threadt=newThread(()->httpCall());t.start();本质是:
- JVM 调用
pthread_create - 创建一个真实的内核线程
- 栈、调度、阻塞全交给操作系统
2️⃣ OS 不懂“这是 IO 等待”
当线程调用:
socket.read();操作系统只知道一件事:
“这个线程现在在等数据”
于是:
- OS 把线程状态设为
BLOCKED - 线程仍然存在
- 仍然占:
- 内核线程结构
- 栈内存
- 调度成本
📌OS 无法把这个线程“拆掉”再给别人用
三、虚拟线程是如何“骗过”操作系统的?
关键思想:
👉阻塞不交给 OS,而是交给 JVM
1️⃣ 虚拟线程的真实结构
Virtual Thread(JVM对象) | |--- Continuation(可挂起的执行体) | |--- 运行在 ↓ Carrier Thread(少量 OS 线程)Carrier Thread 才是真正的 OS 线程
2️⃣ JVM 在关键 IO 点“插手”了
虚拟线程的核心魔法在这里👇
传统线程:
Thread | |-- socket.read() | |-- OS 阻塞线程虚拟线程:
VirtualThread | |-- socket.read() | |-- JVM 拦截 | |-- 保存当前执行现场(Continuation) |-- 从 Carrier Thread 上卸载 |-- 把 Carrier Thread 还给调度器📌OS 完全不知道有这么回事
3️⃣ JVM 怎么知道“这是 IO”?
因为:
👉 JDK 的 IO 被“改造”了(关键)
以下 API 在虚拟线程下是可挂起的:
SocketHttpClientInputStream / OutputStreamSelectorNIO
JDK 内部逻辑(简化):
if(currentThread.isVirtual()){parkContinuation();}else{blockOSThread();}四、Continuation:真正的“黑科技”
1️⃣ 什么是 Continuation?
你可以理解为:
一个可以暂停 / 恢复的调用栈快照
它保存了:
- 当前方法栈
- 局部变量
- 执行位置
2️⃣ 挂起时发生了什么?
┌──────────────┐ │ 方法 A │ │ 方法 B │ │ 方法 C <-- 当前执行点 └──────────────┘JVM 做了:
- 把这段执行栈复制到堆内存
- 解绑当前 OS 线程
- 标记为 WAITING
3️⃣ 恢复时发生了什么?
当 IO 就绪:
- JVM 选一个空闲 Carrier Thread
- 把 Continuation 装回去
- 从 C 方法继续执行
👉就像什么都没发生过
五、为什么说“虚拟线程不占用 OS 线程”?
更准确的说法是:
**虚拟线程在“运行时”才占用 OS 线程
**在“等待时”不占用 OS 线程
对比:
| 状态 | 传统线程 | 虚拟线程 |
|---|---|---|
| 执行 CPU | 占 OS 线程 | 占 OS 线程 |
| 等 IO | 仍占 OS 线程 | ❌ 不占 |
| 空闲 | 占 | 不占 |
六、这就是为什么并发能暴涨 100 倍
假设:
- OS 线程池:200
- IO 等待时间:95%
传统模型:
200 个线程 ≈ 190 个在等 IO ≈ 10 个在干活虚拟线程模型:
200 个 Carrier Thread + 100,000 个 Virtual Thread ≈ 99,500 个在挂起 ≈ 500 个随时可运行七、为什么你“几乎不用改代码”?
因为:
- 同步 API 没变
- try/catch 没变
- ThreadLocal 可用
- JDBC / HTTPClient 可用
JVM在你看不到的地方做了调度革命。