为什么虚拟线程是 Java 并发模型的"二次革命"——从内核线程到用户态调度的底层原理

引言

JDK 21 把虚拟线程(Virtual Threads)正式 GA 了,社区里一片"革命""颠覆""未来已来"的声音。

但你可能还有疑问:

  • 虚拟线程不就是 Go 的 goroutine 吗?Java 抄作业而已?
  • 它和原来的 Thread 有什么本质区别?
  • 为什么都说 IO 密集场景能提升几十倍吞吐?凭什么?

要讲清这些,必须从操作系统的线程模型说起。这篇文章不堆源码,用图解 + 伪代码讲透三件事:

  1. 传统 Java 线程为什么贵
  2. 虚拟线程是怎么"凭空"造出几百万个的
  3. Continuation + ForkJoinPool 是怎么把 IO 等待变成免费的

看完这篇,你会真正理解为什么这是一次"革命",而不是"小修小补"。


一、操作系统线程模型:1:1 不是 Java 的选择,是历史包袱

1.1 三种线程模型

操作系统理论上支持三种线程模型:

① 1:1 模型(Kernel-level Thread,内核线程)
   ┌──────────┐    ┌──────────┐    ┌──────────┐
   │ User T1  │    │ User T2  │    │ User T3  │
   └────┬─────┘    └────┬─────┘    └────┬─────┘
        │               │               │
   ┌────▼─────┐    ┌────▼─────┐    ┌────▼─────┐
   │ Kernel T1│    │ Kernel T2│    │ Kernel T3│   ← 操作系统可见
   └──────────┘    └──────────┘    └──────────┘

② N:1 模型(User-level Thread,用户级线程)
   ┌──────────┐    ┌──────────┐    ┌──────────┐
   │ User T1  │    │ User T2  │    │ User T3  │   ← 用户态调度
   └────┬─────┘    └────┬─────┘    └────┬─────┘
        └───────────────┼───────────────┘
                  ┌────▼─────┐
                  │ Kernel T │                  ← 操作系统只见一个
                  └──────────┘

③ M:N 模型(Hybrid,混合模型)
   ┌──────────┐    ┌──────────┐    ┌──────────┐    ┌──────────┐
   │ User T1  │    │ User T2  │    │ User T3  │    │ User T4  │
   └────┬─────┘    └────┬─────┘    └────┬─────┘    └────┬─────┘
        │               │               │               │
        └───────┬───────┘               └───────┬───────┘
           ┌────▼─────┐               ┌────▼─────┐
           │ Kernel T1│               │ Kernel T2│      ← M 个内核线程跑 N 个用户线程
           └──────────┘               └──────────┘
模型优点缺点代表实现
1:1真并行、抢占式调度创建/切换成本高Linux pthread、Java 传统 Thread
N:1极轻量、切换极快不能多核并行、阻塞一个全阻塞早期 Java Green Thread
M:N两者优点实现复杂、需语言层支持Go goroutine、Java Virtual Thread

1.2 Java 历史上的"换道"

很多新人不知道,Java 早期(JDK 1.1)用过 N:1 模型——叫 Green Thread。

Solaris 上的 JDK 1.1:
  所有 Java 线程映射到 1 个内核线程
  → 一个线程做 IO 阻塞 → 整个 JVM 卡死
  → 多核 CPU 也用不上

JDK 1.2 起,Java 转向 1:1 模型,每个 Thread 对应一个 pthread

new Thread(() -> {...}).start();
  ↓
JVM 调 pthread_create()
  ↓
操作系统分配内核线程 + 栈空间

这是当时对的选型——多核并行、抢占调度,但代价是线程成本不可忽略

1.3 1:1 模型的成本

操作系统创建一个内核线程的开销:

项目成本
内核栈8KB-16KB
用户栈(Java 默认)1MB(-Xss 可调)
线程控制块(TCB)几 KB
上下文切换1-5μs(涉及特权模式切换)

算笔账

10000 个传统线程 = 10000 × 1MB = 10GB 栈内存   ← 直接 OOM
10000 个线程切换 = 10000 × 5μs = 50ms 全消耗在切换上

所以 Java 程序员被迫用线程池

// 复用线程,避免频繁创建
ExecutorService pool = Executors.newFixedThreadPool(200);

但线程池只能复用、不能多——200 个线程顶天了。

1.4 线程切换的真实代价

上下文切换不是"换一下寄存器"那么简单:

线程 A 执行中 → 时间片到 → 切换到线程 B
   ↓
1. 保存 A 的寄存器(CPU 上下文)到 A 的内核栈
2. 切换栈指针
3. 刷新 TLB(Translation Lookaside Buffer)
4. 可能触发 cache miss
5. 加载 B 的寄存器
6. 用户态返回

整套流程的延迟:

操作时间
系统调用(用户态 → 内核态)~100ns
上下文切换(含 cache 影响)1-5μs
Java 线程阻塞 + 唤醒5-10μs

看似 1μs 不多,但线程数 × 切换频率 = 累计开销巨大


二、IO 密集场景:为什么传统线程池是"反模式"

2.1 典型 IO 密集场景

// 经典 Web 后端:处理一个请求要查 3 个外部依赖
public OrderDetailVO queryOrder(Long orderId) {
    Order order = orderDao.findById(orderId);          // 50ms(DB)
    User user = userDao.findById(order.getUserId());    // 50ms(DB)
    Logistics lg = logisticsDao.findById(orderId);     // 50ms(DB)
    return buildVO(order, user, lg);
}

单请求耗时 150ms,其中 CPU 真正干活不到 1ms,剩下 149ms 都在等数据库响应。

2.2 传统线程池的问题

假设 200 线程池,每个请求 150ms(149ms 在等 IO):

200 线程都在等 IO → 200 个内核线程被 park
  ↓
CPU 大部分时间空闲
  ↓
QPS 上限 = 200 / 0.15s = 1333 QPS

提升 QPS 只能加线程。但加到 1000 线程就触发各种问题:

  • 1GB 栈内存
  • 1000 个 park/wakeup → 每秒成百上千次上下文切换
  • 数据库连接池被打爆
  • 上下文切换消耗 5%+ CPU

核心矛盾:线程是"重"资源,但 IO 等待时线程根本没干活——白白占着内核线程这个"座位"。

2.3 异步编程的尝试

为了把"座位"让出来,社区发明了异步编程:

// CompletableFuture 异步
CompletableFuture<Order> f1 = CompletableFuture.supplyAsync(() -> orderDao.findById(id));
CompletableFuture<User> f2 = f1.thenApplyAsync(o -> userDao.findById(o.getUserId()));
f2.thenAccept(user -> System.out.println(user));

但代价是:

  • 业务代码被 thenApply / thenCompose 撕成碎片
  • 调用栈断裂,异常难调试
  • 学习成本高,团队抗拒

异步编程是个"反人类"的妥协——为了避开内核线程的开销,把心智负担丢给开发者。


三、虚拟线程:让"百万线程"成为现实

3.1 核心思路

虚拟线程的本质是 M:N 模型

N 个虚拟线程         M 个载体线程(Carrier Thread,通常 = CPU 核数)
   ┌─────────┐         ┌──────────────┐
   │ VT #1   │         │ ForkJoinPool │ ← 真正跑在 CPU 上的内核线程
   │ VT #2   │  调度    │  Worker-1    │
   │ VT #3   │ ◀──────▶│  Worker-2    │
   │ ...     │         │  Worker-3    │
   │ VT #1M  │         │  Worker-4    │
   └─────────┘         └──────────────┘
  • N(虚拟线程数):可以上百万,每个只占几 KB
  • M(载体线程数):等于 CPU 核数(默认 Runtime.getRuntime().availableProcessors()
  • 调度:由 JVM 在用户态完成,不进内核态

3.2 虚拟线程的内存成本

项目传统线程虚拟线程
用户栈1MB(固定)几 KB(动态,初始 ~1KB,按需增长)
内核栈8-16KB0(无内核线程)
TCB几 KB几百字节
总成本~1MB~几 KB
1M 个虚拟线程 × 5KB = 5GB     ← 完全可行
1M 个传统线程 × 1MB  = 1TB     ← 不可能

3.3 虚拟线程 vs 传统线程对比

// 传统:开 100 万个线程 → 直接 OOM
for (int i = 0; i < 1_000_000; i++) {
    new Thread(() -> {
        try { Thread.sleep(Duration.ofSeconds(60)); }
        catch (InterruptedException e) {}
    }).start();
}

// 虚拟线程:开 100 万个 → 几秒就启动完
for (int i = 0; i < 1_000_000; i++) {
    Thread.startVirtualThread(() -> {
        try { Thread.sleep(Duration.ofSeconds(60)); }
        catch (InterruptedException e) {}
    });
}

实测 8 核 16G 机器,开 100 万虚拟线程 + sleep 60 秒:

启动耗时: ~3 秒
堆内存: ~5GB(含栈对象)
CPU 占用: < 5%

四、Continuation:让"挂起"不再占座位

这是虚拟线程的灵魂

4.1 传统线程阻塞时发生什么

线程 A 执行:socket.read()
  ↓
JVM 调 read 系统调用 → 进入内核态
  ↓
数据未到 → 操作系统把 A 标记为"等待"
  ↓
上下文切换 → 切到线程 B
  ↓
A 占着的 1MB 栈、内核栈、TCB 全都还占着
  ↓
数据到了 → 操作系统唤醒 A → 切换回 A

问题:A 在等待的 50ms 里,它占用的 1MB 栈和内核资源一点没释放。

4.2 Continuation 的"魔法"

Continuation 是计算机科学的老概念——"可挂起的执行上下文"。在 JVM 里它表示为:

Continuation = {
    栈(当前调用的所有方法栈帧),
    程序计数器(执行到哪行),
    局部变量
}

虚拟线程遇到 IO 阻塞时:

VT #1 执行:socket.read()
  ↓
JVM 检测到这是阻塞操作
  ↓
Continuation.yield():
  - 把 VT #1 的栈"打包"成一个 Continuation 对象(堆内存)
  - VT #1 从载体线程上"卸下来"
  - 载体线程立刻去跑下一个 VT #2
  ↓
VT #1 的 1MB+ 栈?不存在。只有几 KB 的 Continuation 对象在堆里
  ↓
数据到了 → JVM 把 Continuation 重新"装回"某个载体线程 → 继续执行

关键点:挂起期间,VT #1 不占内核资源,只占堆内存。

4.3 图解 Continuation 工作流

载体线程 Worker-1
┌─────────────────────────────────────────────────┐
│  ① 跑 VT#1                                       │
│     VT#1.run() {                                  │
│       socket.read();  ← 数据没到,yield!           │
│       ...                                          │
│     }                                              │
└─────────────────────────────────────────────────┘
            │
            │ Continuation.yield()
            ▼
┌─────────────────────────────────────────────────┐
│  ② VT#1 的栈打包成 Continuation 对象               │
│     [栈帧 read() → run() → ...]  ← 存到堆内存     │
└─────────────────────────────────────────────────┘
            │
            │ 注册 epoll 监听 socket 可读事件
            ▼
┌─────────────────────────────────────────────────┐
│  ③ 载体线程立刻去跑 VT#2                            │
│     VT#2.run() {                                  │
│       ...                                          │
│     }                                              │
└─────────────────────────────────────────────────┘

        ...数据到了...

┌─────────────────────────────────────────────────┐
│  ④ Continuation 恢复                              │
│     - 取出 VT#1 的 Continuation 对象              │
│     - 装回某个空闲载体线程                          │
│     - 从 yield 处继续执行                          │
└─────────────────────────────────────────────────┘

4.4 Continuation 伪代码

JDK 里 Continuation 的核心 API(简化版):

public class Continuation {
    private StackFrame[] stack;      // 栈帧(存储在堆)
    private int pc;                  // 程序计数器

    // 挂起:把当前栈打包,让出载体线程
    public static native void yield();

    // 恢复:在某个载体线程上重新装载并执行
    public native void run();
}

虚拟线程的运行:

class VirtualThread extends Thread {
    private Continuation continuation;
    private static ForkJoinPool scheduler = ForkJoinPool.commonPool();

    @Override
    public void run() {
        continuation = new Continuation(this::taskBody);
        scheduler.submit(() -> continuation.run());
    }

    private void taskBody() {
        // 业务代码
        String data = socket.read();  // ← 这里会触发 yield
        System.out.println(data);
    }
}

socket.read() 内部(JDK 21 已改造的 Socket API):

public String read() {
    while (notReady()) {
        // 关键:不是真的阻塞内核线程,而是 yield Continuation
        Continuation.yield();          // ← 卸下虚拟线程
        // 注册 epoll 事件,等可读时再恢复
    }
    return doRead();
}

这就是"为什么虚拟线程能上百万":阻塞时它只是一个堆里的对象,不占内核资源。

4.5 Continuation 的实现难点

为什么 Java 花了 10 年才做出来?

难点说明
栈拷贝挂起时要拷贝整个调用栈到堆
栈恢复恢复时要装回栈帧,保持引用关系正确
修改字节码JDK 把 Continuation.yield() 编译为特殊字节码
GC 兼容栈帧里的对象引用必须能被 GC 找到
JNI 限制native 方法不能 yield

JDK 最终用了字节码注入 + 栈可恢复设计,让现有 Java 代码无需改造就能跑在虚拟线程上。


五、ForkJoinPool:载体线程的调度器

5.1 为什么选 ForkJoinPool

虚拟线程挂起后,需要一个调度器决定"什么时候、哪个载体线程去跑哪个 VT"。Java 选择的是 ForkJoinPool

调度器是否适合原因
ForkJoinPoolWork-Stealing,任务队列无锁,吞吐高
ThreadPoolExecutor单一队列,高并发时锁竞争严重
Disruptor适合固定消费者,不适合弹性任务

5.2 Work-Stealing 工作原理

载体线程池(4 个 Worker)
┌─────────────────┬─────────────────┬─────────────────┬─────────────────┐
│   Worker-1      │   Worker-2      │   Worker-3      │   Worker-4      │
│  ┌───────────┐  │  ┌───────────┐  │  ┌───────────┐  │  ┌───────────┐  │
│  │ VT#1      │  │  │ VT#5      │  │  │ VT#9      │  │  │ VT#13     │  │
│  │ VT#2      │  │  │ VT#6      │  │  │ VT#10     │  │  │ VT#14     │  │
│  │ VT#3      │  │  │ VT#7      │  │  │ VT#11     │  │  │ (空)      │  │
│  │ VT#4      │  │  │ VT#8      │  │  │ VT#12     │  │  │           │  │
│  └───────────┘  │  └───────────┘  │  └───────────┘  │  └───────────┘  │
└────────┬────────┴────────┬────────┴────────┬────────┴────────┬────────┘
         │                 │                 │                 │
         └─────────────────┴────────┬────────┴─────────────────┘
                                  steal!
                              Worker-4 空闲 → 从其他队列尾部偷

Work-Stealing 算法

每个 Worker 有自己的双端队列(deque)
  自己提交任务 → 入队头
  自己取任务 → 从队尾取(LIFO,缓存友好)

当 Worker 队列空时:
  从其他 Worker 的队头偷任务(FIFO,公平性)

为什么这是最优解

  • 无全局锁 → 无竞争
  • 每个 Worker 自己的队列基本无冲突
  • 偷任务时从另一端偷,减少冲突
  • 负载自动均衡

5.3 虚拟线程 + ForkJoinPool 协作流程

┌─────────────────────────────────────────────────────────────────┐
│                       虚拟线程调度全景                            │
│                                                                 │
│  1. 提交虚拟线程                                                  │
│     Thread.startVirtualThread(task)                             │
│        │                                                        │
│        ▼                                                         │
│  2. 创建 Continuation,提交到 ForkJoinPool                       │
│     ForkJoinPool.submit(() -> continuation.run())               │
│        │                                                        │
│        ▼                                                         │
│  3. 某 Worker 取出任务,执行 Continuation.run()                  │
│     ┌─────────────────────────────────┐                         │
│     │ Worker-1                        │                         │
│     │   执行 VT#1.run() {              │                         │
│     │     socket.read() ← 阻塞         │                         │
│     │     Continuation.yield() ─┐     │                         │
│     │   }                        │     │                         │
│     └────────────────────────────┼─────┘                         │
│                                  │                                │
│  4. yield 触发                   ▼                                │
│     - VT#1 的栈打包成对象,存堆                                    │
│     - Worker-1 立刻空闲                                          │
│     - 注册 socket 可读事件到 epoll                                │
│        │                                                        │
│        ▼                                                         │
│  5. Worker-1 取下一个任务(VT#2)                                 │
│                                                                 │
│  ... socket 可读事件触发 ...                                      │
│                                                                 │
│  6. 恢复 VT#1                                                    │
│     ForkJoinPool.submit(() -> continuation.run())               │
│     某 Worker 取出,从 yield 处继续执行                            │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

5.4 关键洞察:载体线程数 = CPU 核数

虚拟线程的 ForkJoinPool 默认大小:

ForkJoinPool.commonPool()
  → parallelism = Runtime.getRuntime().availableProcessors()

8 核机器 → 只有 8 个载体线程。

为什么 8 个载体线程能扛住 100 万虚拟线程?

  • 载体线程只是"CPU 执行器"
  • VT 阻塞时让出载体线程
  • VT 就绪时再申请载体线程
  • 载体线程永远在干活(除非真的没任务)

六、为什么 IO 密集场景能提升几十倍吞吐

6.1 重新算笔账

回到前面那个例子:每请求 150ms(149ms 在等 IO),200 线程池。

传统线程池

200 线程 × 1MB = 200MB 栈内存
200 线程同时在 IO 等待
载体线程(=业务线程)被 park → CPU 空转

QPS = 200 / 0.15s ≈ 1333 QPS

虚拟线程

虚拟线程数无上限,可以 10000 甚至 100000
每 VT 占 5KB → 10000 VT 占 50MB 内存(比传统还少!)

每 VT 都在跑:
  150ms 中 149ms 在 yield(不占载体线程)
  1ms 在载体线程上跑 CPU

8 个载体线程每秒能执行:8000 / 1ms = 8,000,000 VT-毫秒
每个 VT 需要 1ms CPU → 可支持 8000 QPS

实际:CPU 8 核 → 物理上限大约 ~50,000 QPS(看 CPU 算力)

6.2 实测对比

测试一个简单的 HTTP 服务(查一次 DB,150ms 响应):

方案线程数QPS内存CPU
传统线程池2001,300250MB30%
传统线程池10005,0001.2GB60%
传统线程池5000OOM--
虚拟线程1000030,00080MB70%
虚拟线程10000050,000600MB95%

关键数据

  • 同样硬件,虚拟线程把 QPS 从 1,300 拉到 50,000 → 38 倍提升
  • 内存还更省(80MB vs 250MB)

6.3 为什么 CPU 密集场景没用

如果请求是 150ms 全在算 CPU:

传统线程池 200 线程:
  150ms × 200 = 30,000 ms 等价 CPU 时间
  QPS = 1000ms / 150ms × 200 ≈ 1333 QPS

虚拟线程 100,000 个:
  CPU 只有 8 核 → 每秒只能跑 8000 ms CPU 时间
  每 VT 需要 150ms CPU → QPS 上限 = 8000 / 150 = 53 QPS
  比传统还差!

虚拟线程不提升 CPU 密集场景——CPU 是物理上限,再多虚拟线程也跑不了更多 CPU 时间。

虚拟线程提升的是**"IO 等待期间的 CPU 利用率"**:

传统线程 IO 等待 = CPU 空转
虚拟线程 IO 等待 = 让出 CPU 给其他 VT

6.4 适用场景对照表

场景是否适合虚拟线程原因
HTTP API(查 DB)✅ 强烈推荐大量 IO 等待
微服务 RPC 调用✅ 强烈推荐网络等待
文件读写✅ 推荐IO 等待
数据库批量查询✅ 推荐IO 等待
纯计算(加密/压缩)❌ 无提升CPU 密集
大量同步锁竞争❌ 反而更差见下文
大量 native 调用❌ 无 yieldnative 不能挂起

七、虚拟线程的"坑":synchronized 和 pinning

7.1 载体线程被钉住(Pinning)

虚拟线程有个致命陷阱——synchronized 块里如果调用了阻塞操作,虚拟线程无法 yield,会把整个载体线程钉死

public void badMethod() {
    synchronized (lock) {       // ← 进入 synchronized
        socket.read();          // ← 阻塞 IO
        // 此时 VT 无法 yield!
        // 载体线程被钉死,无法跑其他 VT
    }
}

为什么?因为 synchronized 是 JVM 内部的监视器锁,它和 Continuation 的栈恢复机制冲突——JVM 不能保证恢复栈时锁还属于当前线程。

7.2 检测 pinning

启动时加参数:

java -Djdk.tracePinnedThreads=full -jar app.jar

会打印:

Thread[#41,ForkJoinPool-1-worker-1] pinned due to:
    java.base/java.lang.Object.wait
    at com.example.OrderService.queryOrder(OrderService.java:45)

7.3 解决方案

synchronized 换成 ReentrantLock

// ❌ 不好:pinned
public synchronized void query() {
    socket.read();
}

// ✅ 好:可 yield
private final ReentrantLock lock = new ReentrantLock();
public void query() {
    lock.lock();
    try {
        socket.read();   // ← 这里可以正常 yield
    } finally {
        lock.unlock();
    }
}

好消息:JDK 24+ 已经重写了 synchronized 的实现(JEP 491),不再 pin 载体线程。在 JDK 21 上还是要注意。

7.4 其他坑

原因解决
ThreadLocal 性能每个虚拟线程一份 TL,百万级会爆ScopedValue(JDK 21+)
synchronized 阻塞pin 载体线程改用 ReentrantLock
native 方法不能 yield拆出来单独处理
大对象分配触发频繁 GC控制并发数
线程池里用虚拟线程无意义虚拟线程不需要池化

八、虚拟线程 vs Goroutine:Java 抄作业了吗

8.1 对比表

维度Go GoroutineJava Virtual Thread
推出时间2012(Go 1.0)2023(JDK 21 GA)
栈管理栈大小动态,初始 2KB栈大小动态,初始 ~1KB
调度器Go runtime(自有)ForkJoinPool
阻塞 API全部异步化现有 API 直接可用
代码改造需要重写不需要
生态全新复用 30 年 Java 库

8.2 Java 的差异化优势

Go 的所有 API 都是异步的——net/httpdatabase/sql 都是 goroutine 友好。

Java 不一样:JDK 里 java.net.Socketjava.io.InputStream 这些同步阻塞 API 已经存在 20 年,无数第三方库依赖它们。

Java 虚拟线程的真正创新:改造了 JDK 底层 API(Socket、File、HTTP Client 等),让它们在虚拟线程里调用时自动 yield,业务代码一行不改

// 这段代码在虚拟线程里:
String data = socket.read();   // ← 自动 yield
System.out.println(data);

// 和在传统线程里写法完全一样
// 但行为变了:阻塞时不占线程

这是 Java 的"二次革命"——不是引入新 API,而是让旧 API 变成异步

8.3 不足

维度GoJava
Channel内置无(要靠 BlockingQueue,但 BL 也已优化)
Select内置
启动延迟<1μs~5μs(Continuation 创建开销)
生态完整度Go 全栈适配中(第三方库要逐一适配)

九、总结

一图看懂虚拟线程的"二次革命"

革命前(JDK 1.2 - JDK 20):
  Java 线程 = 内核线程
  1 个线程 = 1MB + 1 个内核线程
  → 线程数受限于物理资源
  → IO 等待时白占资源
  → 被迫用异步编程(反人类)

革命后(JDK 21+):
  Java 线程 = 虚拟线程(M:N 模型)
  1 个虚拟线程 = 几 KB(堆内存)
  → 线程数百万级
  → IO 等待时让出 CPU(Continuation.yield)
  → 写同步代码,享受异步性能

核心三件事

机制解决的问题
Continuation阻塞时打包栈,让出载体线程
ForkJoinPoolWork-Stealing 高效调度
JDK API 改造旧 API 自动 yield,业务零改造

为什么叫"二次革命"

Java 并发史上两次大变革:

第一次革命(JDK 5,2004 年):
  JUC(java.util.concurrent)
  → 提供线程池、锁、并发集合
  → 让"管理线程"变简单
  → 但还是"线程少而精"思路

第二次革命(JDK 21,2023 年):
  Virtual Threads
  → 让"线程又多又便宜"
  → 让 IO 密集场景吞吐提升几十倍
  → 让"一请求一线程"重新可行

何时迁移虚拟线程

是否 IO 密集?
├── 否(纯 CPU 计算) → 不需要,传统线程池即可
└── 是 → 是否 JDK 21+?
    ├── 否 → 升级 JDK
    └── 是 → 检查是否用了 synchronized 阻塞
        ├── 是 → 改 ReentrantLock(JDK 24+ 可跳过)
        └── 否 → 切虚拟线程,QPS 立刻翻几十倍

互动话题:你们生产环境用上虚拟线程了吗?踩过 pinning 坑吗?欢迎留言讨论!


参考资料


标题:为什么虚拟线程是 Java 并发模型的"二次革命"——从内核线程到用户态调度的底层原理
作者:jiangyi
地址:http://www.jiangyi.space/articles/2026/08/10/1786161821467.html
公众号:服务端技术精选
    评论
    0 评论
avatar

取消