VirtualThread / Continuation / ScopedValue 结构化并发源码
概述
虚拟线程(JDK 21 正式落地)解决了"线程数量受限于 OS 线程"的问题:一个平台线程可以同时承载成千上万个虚拟线程,虚拟线程的挂起/恢复由 JVM 内部的 Continuation(延续体) 机制实现——挂起时把栈帧整体保存到堆内存,恢复时再装回去,全程不阻塞底层 OS 线程。
ScopedValue 则是与虚拟线程配套的结构化传值方案:在调用边界内绑定一个不可变值,作用域结束自动恢复,替代 ThreadLocal 在虚拟线程场景下的泄漏与继承问题。
本文基于 OpenJDK 21 源码,从 Thread.ofVirtual() 的创建链路开始,拆解 VirtualThread 的载体机制、Continuation 的挂起恢复原语,最后深入 ScopedValue 的绑定与快照实现。
核心源码解析
① Thread.ofVirtual().start(Runnable) 的虚拟线程创建
// Thread.java
public static Builder.OfVirtual ofVirtual() {
return new VirtualThreadBuilder();
}VirtualThreadBuilder(实现 Thread.Builder.OfVirtual)的 start(Runnable):
// VirtualThreadBuilder
public Thread start(Runnable task) {
return Thread.start(this, task);
}
// Thread.start(Thread.Builder builder, Runnable task)
private static Thread start(Thread.Builder builder, Runnable task) {
Thread thread = builder.build(task); // VirtualThreadBuilder.build → new VirtualThread(...)
thread.start(); // 虚拟线程启动
return thread;
}VirtualThread 的核心字段:
final class VirtualThread extends BaseVirtualThread {
private static final ForkJoinPool DEFAULT_SCHEDULER = createDefaultScheduler();
private final Executor scheduler; // 调度器(默认 ForkJoinPool)
private final Object lock = new Object();// 同步锁
private final Runnable task; // 用户任务
private final long threadId; // 虚拟线程唯一 id
private volatile int state; // NEW/STARTED/RUNNABLE/PARKED/TERMINATED 等
private Continuation cont; // 延续体:挂起/恢复的核心
private Object carrierThread; // 当前载体平台线程
private boolean mounted; // 是否已挂载到载体线程
...
}- 不映射 OS 线程:创建虚拟线程只分配一个
Continuation对象与少量字段,开销远小于平台线程(平台线程需要内核栈与 OS 资源)。 state字段是虚拟线程的运行时状态机,lock用于状态转换同步。VirtualThreadBuilder还支持name()/unstarted(Runnable)/inheritInheritableThreadLocals(false)等配置。
② VirtualThread.run() 的载体机制
虚拟线程本身不直接执行任务,而是由**载体线程(carrier thread)**代为执行,这个"代为执行"动作就是 mount / unmount:
private void mount() {
// ① 记录载体线程
carrierThread = Thread.currentThread();
// ② 上下文切换:把载体线程的线程局部状态"移交"给虚拟线程
Thread carrier = (Thread) carrierThread;
carrier.setCurrentThread(this); // 载体内部 currentThread 指向虚拟线程
// ③ 关键:synchronized 等持有锁的场景会 pin 住载体
...
mounted = true;
}
private void unmount() {
// 反向切换:把虚拟线程的上下文归还载体线程
Thread carrier = (Thread) carrierThread;
carrier.setCurrentThread(carrier);
carrierThread = null;
mounted = false;
}Continuation.run():载体线程调用cont.run()进入延续体执行用户代码。- 阻塞即让位:虚拟线程遇到
park(锁等待、IO 等待)时unmount并让出载体线程,载体线程转而去执行调度队列中的其他虚拟线程;恢复时再mount到某个空闲载体上继续执行。 - pin 机制:当虚拟线程持有
synchronized监视器(或执行 native 方法)时会被 pin——无法unmount,此时阻塞会连带阻塞载体线程。JDK 21 中对象监视器尚不支持虚拟线程让位,但ReentrantLock等java.util.concurrent锁已支持。
③ Continuation.yield(ForkJoinPool) 的挂起
jdk.internal.vm.Continuation 是虚拟线程的底层原语(JDK 21 前的原型版本中类名为 Fiber):
public class Continuation {
private final ContinuationScope scope; // 延续体作用域(标识所属虚拟线程)
private final Runnable target; // 首次进入时执行的目标
private boolean done; // 是否已完成
private boolean yielded; // 是否已挂起
...
public void run() {
enter(); // 进入:首次执行 target
}
private native boolean enter0();
public static boolean yield(ContinuationScope scope) {
return yield0(scope);
}
private static native boolean yield0(ContinuationScope scope);
static native void continue0(ContinuationScope scope);
}yield 的 JVM 侧动作:
- 保存栈帧:把当前线程(载体)从虚拟线程栈顶往下到载体栈的全部 Java 栈帧拷贝到堆内存(
Continuation的 chunk 存储),构成"挂起点之后继续执行"所需的完整状态。 - 切换到父栈:栈指针切回载体线程的栈,
yield0返回。 - 失败返回:若当前被 pin(如持有对象监视器),
yield0返回false,虚拟线程只能阻塞载体线程等待唤醒。
④ Continuation.run() 的恢复
恢复路径的 native 链:
VirtualThread.unpark() / 调度器执行
→ runContinuation() → cont.run() → enter() → enter0()(JVM 已进入过该延续体)
→ continue0(scope):JVM 从 chunk 恢复保存的栈
→ resume():从上次 yield 的断点继续执行- 首次进入 vs 恢复:
enter0首次进入时执行target;若yielded == true,则continue0把保存的栈 chunk 装回载体线程的栈,虚拟线程从上次挂起的字节码位置(yield 的下一条指令)继续执行。 - 与
Thread.sleep/LockSupport.park不同,恢复不需要新建线程——载体线程(可能是另一个平台线程)直接复用保存的栈帧继续跑,这正是"百万虚拟线程"能成立的关键。 VirtualThread内部的runContinuation会先尝试"直接在当前载体上运行"(runContinuation0优化),失败才提交给调度器。
⑤ VirtualThread 的调度器
private static ForkJoinPool createDefaultScheduler() {
ForkJoinPool pool;
try {
pool = new ForkJoinPool(getDefaultParallelism(), factory, null, false);
} catch (Throwable t) {
pool = null; // 极端情况下退化为单线程执行
}
return pool;
}DEFAULT_SCHEDULER是ForkJoinPool,parallelism默认Runtime.getRuntime().availableProcessors()——即调度队列的并行度与 CPU 核数一致,避免平台线程数量膨胀。- 虚拟线程启动时
scheduler.execute(runContinuation)把任务交给 ForkJoinPool;挂起/恢复时由池内工作线程执行mount → cont.run() → unmount循环。 - 定制调度器:
Thread.ofVirtual().scheduler(Executor)可指定自己的调度器(如测试用单线程池),生产环境通常保持默认。 - 与平台线程的
ThreadPoolExecutor不同,虚拟线程调度是"M 个载体线程承载 N 个虚拟线程"的共享执行模型,ForkJoinPool 的 work-stealing 天然适合这种高并发挂起/恢复场景。
⑥ VirtualThread.join() 的 park/unpark
join() 内部依赖虚拟线程的 park/unpark 原语(继承自 BaseVirtualThread):
// park:挂起当前虚拟线程
@Override
void park() {
if (pinReason != null) { // 被 pin:只能阻塞载体线程
...
} else {
setState(PARKED);
try {
if (!Continuation.yield(VTHREAD_SCOPE)) {
// yield 失败(意外 pin),退化为阻塞载体
...
}
} finally {
setState(RUNNABLE);
}
}
}
// unpark:唤醒
@Override
void unpark() {
if (getState() == PARKED) {
submitRunContinuation(); // 提交到调度器恢复执行
}
}park()→Continuation.yield:保存栈帧并让出载体线程,虚拟线程进入 PARKED。unpark()→submitRunContinuation():把该虚拟线程的 continuation 重新提交给调度器,载体线程执行cont.run()恢复。join()的完整语义:while (!thread.isDone()) { thread.unpark(); park(); }——join 方挂起,被 join 线程结束或被打断时唤醒。由于挂起不占 OS 线程,大量线程同时 join 也不会产生线程风暴。
⑦ ScopedValue.where(K key, V value) 的绑定
public static <T> Carrier where(ScopedValue<T> key, T value) {
return new Carrier(key, value);
}
public static final class Carrier {
private final ScopedValue<?> key;
private final Object value;
...
public void run(Runnable op) {
// 基于当前绑定快照生成新绑定,执行 op,结束后恢复
ScopedValueMap newMap = new ScopedValueMap();
newMap.put(key, value);
runWith(newMap, op);
}
public <U> U call(Callable<U> op) { ... } // 带返回值的版本
}ScopedValue.get() 的查找链:
ScopedValue.get()
→ findBinding():先在当前线程的 Thread.scopedValueBindings 找
→ 再查当前 Continuation 的绑定表(虚拟线程场景)
→ 命中返回 value,未命中抛 NoSuchElementException(或 orElse 兜底)- 绑定是层级作用域:
ScopedValue.where(k, v).run(op)在op执行期间get()可见,op返回后绑定自动失效——无论op正常返回还是抛异常。 - 链式绑定:
ScopedValue.where(k1, v1).where(k2, v2).run(op)组合多个键值,内部构建一个包含全部绑定的新 map。 - 动态作用域语义:绑定对
op内调用的所有方法(含新建的虚拟线程/平台线程)可见,这是"结构化并发传值"的核心——值随调用栈传播而非随线程传播。
⑧ ScopedValue 的 Snapshot 快照
// jdk.internal.misc.ScopedValueSupport
public static Object[] getScopedValueBindings() {
return Thread.scopedValueBindings(); // 返回当前绑定键值对的 Object[]
}
// Carrier.runWith 的简化逻辑
private void runWith(ScopedValueMap newMap, Runnable op) {
Object[] snapshot = getScopedValueBindings(); // ① 保存快照
try {
Thread.setScopedValueBindings(newMap); // ② 安装新绑定
op.run(); // ③ 执行任务
} finally {
Thread.setScopedValueBindings(snapshot); // ④ 恢复快照
}
}- 快照是
Object[]:scopedValueBindings()把当前所有绑定打包成"键值对平铺数组"([k1, v1, k2, v2, ...]),restore时整体写回。 - try/finally 保证恢复:
run/call用finally恢复快照,因此异常也不会泄漏绑定——这是与ThreadLocal的"手动 remove / 线程复用导致脏值"问题的本质区别。 - 虚拟线程场景:绑定快照随
Continuation的挂起/恢复一起保存(绑定表挂在线程或延续体上),切换载体线程时绑定仍保持一致。
⑨ ScopedValue vs ThreadLocal
| 维度 | ThreadLocal | ScopedValue |
|---|---|---|
| 可变性 | set() 随时修改 | 不可变:创建时确定,运行中不能改 |
| 生命周期 | 随线程,需手动 remove() | 随作用域,run() 结束自动恢复 |
| 泄漏风险 | 线程池复用可能读到脏值 | 无(finally 恢复快照) |
| 继承 | InheritableThreadLocal 创建时拷贝一次 | 动态作用域,随调用传播到子任务 |
| 传值方式 | 按线程共享可变状态 | 按调用边界绑定不可变值 |
| 虚拟线程 | 需要小心清理 | 结构化并发首选 |
ScopedValue与虚拟线程是互补设计:虚拟线程解决"线程数量",ScopedValue解决"跨线程传递上下文且不泄漏"。- 限制:不允许
set()(只能where新建绑定)、get()未绑定时抛NoSuchElementException(可用orElse/orElseThrow兜底)、序列化不受支持。
总结
结构化并发的三条核心机制构成虚拟线程的完整图景:
| 机制 | 组件 | 作用 |
|---|---|---|
| 线程抽象 | VirtualThread + VirtualThreadBuilder | 轻量线程对象,不映射 OS 线程 |
| 挂起/恢复 | Continuation(enter / yield0 / continue0) | 栈帧整体保存/恢复,挂起不占平台线程 |
| 调度 | ForkJoinPool(DEFAULT_SCHEDULER) | 载体线程共享执行,work-stealing 负载均衡 |
| 传值 | ScopedValue(Carrier + Snapshot) | 不可变绑定 + 作用域自动恢复,无泄漏 |
虚拟线程的本质是用户态栈切换:Continuation.yield 把栈帧存到堆,continue0 再装回去;载体线程只负责"运输"这些栈。而 ScopedValue 用动态作用域替代线程局部状态,让上下文传递与虚拟线程的载体切换解耦。理解这层 JVM native 原语 + 用户态状态机 的组合,就掌握了 JDK 21 结构化并发的全部底层原理。