Skip to content

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 内存
创建时间~1msOS 系统调用
上下文切换~1μsOS 内核态切换
最大并发数千受限于 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μs1000x
上下文切换~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 会用虚拟线程处理请求,每个请求一个虚拟线程
  • : 虚拟线程的调试和监控有变化吗?

    • : jstack 可以同时显示虚拟线程和平台线程。JFR 支持虚拟线程事件。IDEA 2023.3+ 支持虚拟线程调试。注意:Java Flight Recorder 可以给出虚拟线程的挂起原因

📚 相关链接

  • **Java虚拟机栈** — 虚拟线程的"栈"与平台线程栈的区别
  • **JMM与happens-before** — JMM 在虚拟线程下同样适用
  • **ZGC原理** — 另一个全并发特性
  • **OOM排查方法论** — 虚拟线程过多可能导致堆 OOM(栈帧在堆上)
  • ← 返回 **前沿索引**

Knowledge4J — Java 知识库