Java虚拟线程(VirtualThread)使用详解
目录
- 流程概要
- 调度器继承
- Continuation 类深度分析
- 实现多个虚拟线程的 JVM 级别调度还需要什么?
- 如果自己设计虚拟线程调度应该怎么做?
- 总结
VirtualThread 是 Java 实现轻量级并发(也称为纤程或协程)的关键。与传统的平台线程(直接映射到操作系统线程)不同,虚拟线程由 JVM 管理和调度,可以在少量平台线程上运行大量的虚拟线程,从而提高应用程序的吞吐量。
流程概要
核心组件:
scheduler (Exechttp://www.devze.comutor): 每个虚拟线程都有一个调度器,它是一个 Executor 实例。
这个调度器负责安排虚拟线程在载体线程(通常是平台线程)上执行。
如果没有显式指定,它会使用默认的 ForkJoinPool (DEFAULT_SCHEDULER) 或者继承父虚拟线程的调度器。
- VirtualThread.java
// scheduler and continuation private final Executor scheduler; private final Continuation cont; private final Runnable runContinuation;
在构造函数中,如果 scheduler 为 null,会进行选择:
- VirtualThread.java
// ... existing code ...
VirtualThread(Executor scheduler, String name, int characteristics, Runnable task) {
super(name, characteristics, /*bound*/ false);
Objects.requireNonNull(task);
// choose scheduler if not specified
if (scheduler == null) {
Thread parent = Thread.currentThread();
if (parent instanceof VirtualThread vparent) {
scheduler = vparent.scheduler;
} else {
scheduler = DEFAULT_SCHEDULER;
}
}
this.scheduler = scheduler;
this.cont = new VThreadContinuation(this, task);
this.runContinuation = this::runContinuation;
}
// ... existing code ...
cont (Continuation): 这是虚拟线程的核心。Continuation 是 JDK 内部的一个机制,允许代码执行被挂起(yield)和恢复。每个虚拟线程都包装了一个 Continuation 实例。
VThreadContinuation 是一个内部类,它继承自 Continuation,并包装了用户提供的 Runnable 任务。
- VirtualThread.java
// ... existing code ...
/**
* The continuation that a virtual thread executes.
*/
private static class VThreadContinuation extends Continuation {
VThreadContinuation(VirtualThread vthread, Runnable task) {
super(VTHREAD_SCOPE, wrap(vthread, task));
}
@Override
protected void onPinned(Continuation.Pinned reason) {
}
private static Runnable wrap(VirtualThread vthread, Runnable task) {
return new Runnable() {
@Hidden
@JvmtiHideEvents
public void run() {
vthread.notifyJvmtiStart(); // notify JVMTI
try {
vthread.run(task);
} finally {
vthread.notifyJvmtiEnd(); // notify JVMTI
}
}
};
}
}
// ... existing code ...
runContinuation (Runnable): 这是一个 Runnable,其 run() 方法(即 this::runContinuation 指向的 VirtualThread#runContinuation() 方法)负责实际执行或继续执行虚拟线程的任务。它处理虚拟线程的挂载(mount)到载体线程、运行 Continuation、以及卸载(unmount)。
状态管理: VirtualThread 内部维护了一系列状态常量(如 NEW, STARTED, RUNNING, PARKED, TERMINATED 等)和一个 volatile int state 字段来跟踪其生命周期。状态之间的转换是精心设计的,以处理各种场景,如启动、运行、暂停、阻塞和终止。
- VirtualThread.java
// ... existing code ... /* * Virtual thread state transitions: * * NEW -> STARTED // Thread.start, schedule to run * STARTED -> TERMINATED // failed to start * STARTED -> RUNNING // first run * RUNNING -> TERMINATED // done * * RUNNING -> PARKING // Thread parking with LockSupport.park // ... many more states ... * YIELDED -> RUNNING // continue execution after Thread.yield */ private static final int NEW = 0; private static final int STARTED = 1; private static final int RUNNING = 2; // runnable-mounted // untimed and timed parking pri编程vate static final int PARKING = 3; private static final int PARKED = 4; // unmounted // ... other state constants ... private static final int TERMINATED = 99; // final state // ... existing code ...
carrierThread (Thread): 表示当前承载该虚拟线程执行的平台线程。当虚拟线程被挂起(unmounted)时,它不占用平台线程。当它需要运行时,调度器会将其调度到某个可用的载体线程上执行。
启动流程 (start() 方法):
- VirtualThread.java
// ... existing code ...
@Override
void start(ThreadContainer container) {
if (!compareAndSetState(NEW, STARTED)) {
throw new IllegalThreadStateException("Already started");
}
// bind thread to container
assert threadContainer() == null;
setThreadContainer(container);
// start thread
boolean addedToContainer = false;
boolean started = false;
try {
container.add(this); // may throw
addedToContainer = true;
// scoped values may be inherited
inheritScopedValueBindings(container);
// submit task to run thread, using externalSubmit if possible
externalSubmitRunContinuationOrThrow();
started = true;
} finally {
if (!started) {
afterDone(addedToContainer);
}
}
}
@Override
public void start() {
start(ThreadContainers.root());
}
// ... existing code ...
- 当调用虚拟线程的
start()方法时,它首先将状态从NEW原子地更新为STARTED。 - 然后,它将自身添加到一个线程容器 (
ThreadContainer) 中。 - 最关键的一步是调用
externalSubmitRunContinuationOrThrow()(或类似的提交方法) 将runContinuation任务提交给其scheduler执行。
执行与挂起 (runContinuation() 和 yieldContinuation()):
- VirtualThread.java
// ... existing code ...
@ChangesCurrentThread // allow mount/unmount to be inlined
private void runContinuation() {
// the carrier must be a platform thread
if (Thread.currentThread().isVirtual()) {
throw new WrongThreadException();
}
// set state to RUNNING
int initialState = state();
if (initialState == STARTED || initialState == UNPARKED
|| initialState == UNblockED || initialState == YIELDED) {
// newly started or continue after parking/blocking/Thread.yield
if (!compareAndSetState(initialState, RUNNING)) {
return;
}
// consume permit when continuing after parking or blocking. If continue
// after a timed-park or timed-wait then the timeout task is cancelled.
if (initialState == UNPARKED) {
cancelTimeoutTask();
setParkPermit(false);
} else if (initialState == UNBLOCKED) {
cancelTimeoutTask();
blockPermit = false;
}
} else {
// not runnable
return;
}
mount();
try {
cont.run();
} finally {
unmount();
if (cont.isDone()) {
afterDone();
} else {
afterYield();
}
}
}
// ... existing code ...
- VirtualThread.java
// ... existing code ...
@Hidden
private boolean yieldContinuation() {
notifyJvmtiUnmount(/*hide*/true);
try {
return Continuation.yield(VTHREAD_SCOPE);
} finally {
notifyJvmtiMount(/*hide*/false);
}
}
// ... existing code ...
runContinuation():
- 检查当前线程是否为平台线程(载体线程不能是虚拟线程)。
- 原子地更新状态到
RUNNING。 - 调用
mount()方法,将虚拟线程“挂载”到当前平台线程上。这包括设置carrierThread字段,并使Thread.currentThread()返回该虚拟线程实例。 - 执行
cont.run(),这会实际运行或恢复Continuation中包装的用户任务。 - 执行完毕或
Continuation挂起后,调用unmount()方法,将虚拟线程从平台线程上“卸载”。这包括清除carrierThread,并使Thread.currentThread()返回平台线程本身。 - 根据
Continuation是否完成 (isDone()),调用afterDone()或afterYield()。 yieldContinuation(): 当虚拟线程需要挂起时(例如,执行了LockSupport.park(),或者I/O操作会阻塞),它会调用Continuation.yield(VTHREAD_SCOPE)。这会导致Continuation的执行暂停,runContinuation()方法中的cont.run()调用会返回。此时,载体平台线程可以被释放去执行其他任务。afterYield(): 当yieldContinuation()成功挂起后,此方法被调用。它根据虚拟线程挂起前的原因(如PARKING,YIELDING,BLOCKING,WAITING)更新状态,并可能安排后续操作(如设置定时器唤醒、重新提交到调度器等)。
Parking (park() 和 parkNanos()):
- VirtualThread.java
// ... existing code ...
@Override
void park() {
assert Thread.currentThread() == this;
// complete immediately if parking permit available or interrupted
if (getAndSetParkPermit(false) || interrupted)
return;
// park the thread
boolean yielded = false;
setState(PARKING);
try {
yielded = yieldContinuation();
} catch (OutOfMemoryError e) {
// park on carrier
} finally {
assert (Thread.currentThread() == this) && (yielded == (state() == RUNNING));
if (!yielded) {
assert state() == PARKING;
setState(RUNNING);
}
}
// park on the carrier thread when pinned
if (!yielded) {
parkOnCarrierThread(false, 0);
}
}
// ... existing code ...
- 当虚拟线程调用
LockSupport.park()或LockSupport.parkNanos()时,会调用VirtualThread内部的park()或parkNanos()方法。 - 这些方法会尝试通过
yieldContinuation()来挂起虚拟线程。 - 如果
yieldContinuation()成功(即虚拟线程没有被固定在载体线程上),虚拟线程的状态会变为PARKED或TIMED_PARKED,并且载体线程被释放。 - 如果
yieldContinuation()失败(例如,虚拟线程在 JNI 调用中或synchronized块中被固定),它会退而求其次,在当前的载体线程上实际执行U.park()(通过parkOnCarrierThread方法),此时载体线程会被阻塞,状态变为PINNED或TIMED_PINNED。
调度 (submitRunContinuation 系列方法):
- VirtualThread.java
// ... existing code ...
private void submitRunContinuation(Executor scheduler, boolean retryOnOOME) {
boolean done = false;
while (!done) {
try {
// Pin the continuation to prevent the virtual thread from unmounting
// when submitting a task. For the default scheduler this ensures that
// the carrier doesn't change when pushing a task. For other schedulers
// it avoids deadlock that could arise due to carriers and virtual
// threads contending for a lock.
if (currentThread().isVirtual()) {
Continuation.pin();
try {
scheduler.execute(runContinuation);
} finally {
Continuation.unpin();
}
} else {
scheduler.execute(runContinuation);
}
done = true;
} catch (RejectedExecutionException ree) {
submitFailed(ree);
throw ree;
} catch (OutOfMemoryError e) {
if (retryOnOOME) {
U.park(false, 100_000_000); // 100ms
} else {
throw e;
}
}
}
}
// ... existing code ...
- 有多种提交
runContinuation任务到调度器的方法,如submitRunContinuation(),lazySubmitRunContinuation(),externalSubmitRunContinuation()。 - 这些方法处理了向调度器提交任务的细节,包括在特定情况下(如在虚拟线程内部提交任务给调度器)需要固定
Continuatio编程客栈n(Continuation.pin()) 以防止在提交过程中虚拟线程被意外卸载。
总而言之,VirtualThread 的实现巧妙地结合了 Continuation 机制和 Executor 调度框架。Continuation 提供了挂起和恢复执行流的能力,使得虚拟线程在阻塞操作时可以释放底层的平台线程。调度器则负责在虚拟线程准备好运行时,将其分配给一个可用的平台线程执行。通过这种方式,少量的平台线程就可以支持大量并发的虚拟线程,大大降低了线程创建和上下文切换的开销。状态管理确保了虚拟线程在各种并发场景下的正确行为。
调度器继承
instanceof 模式匹配语法 (parent instanceof VirtualThread vparent)
这是一种 Java 的语法特性,称为 模式匹配 (Pattern Matching) for instanceof。它从 Java 14 开始作为预览特性引入,并在 Java 16 中正式发布。
在以前的 Java 版本中,如果想检查一个对象的类型,并且如果类型匹配则将其转换为该类型并使用,你通常会这样写:
if (parent instanceof VirtualThread) {
VirtualThread vparent = (VirtualThread) parent;
// 然后使用 vparent
scheduler = vparent.scheduler;
}
模式匹配 instanceof 简化了这个过程。
if (parent instanceof VirtualThread vparent) 这行代码做了两件事:
- 类型检查: 它检查
parent对象是否是VirtualThread的一个实例。 - 条件声明和赋值: 如果
parent确实是VirtualThread的实例,那么它会声明一个新的局部变量vparent(类型为VirtualThread),并将parent自动转换 (cast) 为VirtualThread类型后赋值给vparent。这个vparent变量只在if语句块为真(即类型匹配成功)的作用域内有效。
这是一种更简洁、更安全的写法,避免了显式的类型转换和引入额外的变量声明步骤。
这里的“父子关系”并不是指操作系统层面严格的父子进程或线程关系,而是指创建者与被创建者的关系。
Thread parent = Thread.currentThread();:这行代码获取的是当前正在执行 VirtualThread 构造函数的线程。这个线程就是新虚拟线程的“创建者”或“父”线程。
逻辑:
当创建一个新的 VirtualThread 时,可以显式地给它传递一个 scheduler。
如果调用者没有提供 scheduler (即 scheduler == null),那么虚拟线程的构造逻辑会尝试确定一个默认的调度器。
这时,它会检查创建这个新虚拟线程的线程 (parent):
- 如果
parent本身也是一个VirtualThread(parent instanceof VirtualThread vparent):那么新的虚拟线程将继承其创建者虚拟线程的调度器 (scheduler = vparent.scheduler;)。 - 如果
parent是一个平台线程 (platform thread) 或者其他非VirtualThread类型: 那么新的虚拟线程将使用默认的调度器 (scheduler = DEFAULT_SCHEDULER;),这个默认调度器通常是一个ForkJoinPool。
这种设计体现了一种“上下文感知”的默认行为。如果你的代码已经在某个特定的虚拟线程(它使用着特定的调度器)中运行,当你从这个虚拟线程中再创建一个新的虚拟线程时,让新的虚拟线程默认使用与创建者相同的调度器通常是合理的。这有助于:
- 资源管理: 如果你为一组相关的任务配置了特定的调度器(例如,具有特定线程池大小或优先级的调度器),那么从这个组内派生的新虚拟线程默认使用相同的调度器可以保持资源使用的一致性。
- 行为一致性: 任务的执行特性(如并发级别)可以更容易地在相关的虚拟线程间保持一致。
简单来说,如果一个虚拟线程A创建了另一个虚拟线程B,并且没有为B指定调度器,那么B就会默认使用A的调度器。如果是一个平台线程创建了虚拟线程B,并且没有为B指定调度器,那么B就会使用全局默认的调度器。
Continuation 类深度分析
Continuation 类是 Java 实现轻量级线程(虚拟线程)的基石。它代表了一种一次性(one-shot)的分界延续(delimited continuation)。简单来说,它封装了一段计算(一个 Runnable 任务),这段计算可以被挂起(yield),并在之后从挂起点恢复执行。
核心能力与特性:
封装计算单元:
- 每个
Continuation对象都关联一个Runnable任务 (this.target) 和一个ContinuationScope(this.scope)。 ContinuationScope用于界定yield操作的范围。当调用Continuation.yield(scope)时,执行会从当前Continuation向上回溯,直到找到匹配该scope的Continuation实例,然后挂起。
执行与挂起 (run() 和 yield()):
run(): 这是启动或恢复 Continuation 执行的入口点。
- 它首先会尝试“挂载”(mount)
Continuation到当前的载体线程(carrier thread)。挂载意味着将Continuation的执行上下文(主要是栈帧)与载体线程关联起来。 - 通过
JLA.setContinuation(t, this)将当前Continuation设置为载体线程的活动Continuation。 - 调用本地方法
enterSpecial(this, isContinue, isVirtualThread)来实际进入或恢复Continuation的执行。这个本地方法是 JVM 实现Chttp://www.devze.comontinuation魔法的核心,它处理栈的切换和管理。 enterSpecial内部会调用enter(),进而调用target.run()来执行用户代码。- 当
Continuation内部调用Continuation.yield(scope)时,enterSpecial(或其调用的更深层本地代码) 会保存当前执行状态(栈帧等),然后“返回”到run()方法中enterSpecial调用的地方。 run()方法的finally块负责“卸载”(unmount)Continuation,清理状态,并将载体线程的活动Continuation恢复为其父Continuation(如果存在)。
yield(ContinuationScope scope) (静态方法):
- 这是
Continuation主动让出执行权的方式。 - 它会找到当前线程上与指定
scope匹配的最内层Continuation。 - 然后调用该
Continuation实例的yield0(scope, child)方法(这是一个非静态的内部方法,最终会触发本地代码doYield()),将执行权交还给其父Continuation或调度器。 yieldInfo字段用于在yield时传递信息,决定是彻底返回还是在父Continuation中继续yield。
栈管理 (StackChunk):
Continuation的执行栈不是直接使用平台线程的完整栈,而是由一系列StackChunk对象来管理。当Continuation挂起时,它的活动栈帧被保存在这些StackChunk中。恢复时,这些栈帧被重新加载。private StackChunk tail;字段指向Continuation栈的当前末端。isEmpty()方法检查所有StackChunk是否都为空,用于判断Continuation是否执行完毕。
状态管理:
done: 标记Continuation的Runnable是否已经执行完毕。mounted: 一个volatile标志,表示Continuation当前是否挂载在某个载体线程上。mount()和unmount()方法以及compareAndSetMounted()原子地更新此状态。scopedValueCache: 用于支持ScopedValue,在Continuation挂载和卸载时保存和恢复作用域值缓存。
父子关系 (parent, child):
Continuation可以形成一个层级结构。当一个Continuation(父) 内部的某个点创建并运行另一个Continuation(子) 时,它们之间就建立了父子关系。JLA.getContinuation(currentCarrierThread())用于获取当前载体线程上活动的Continuation。run()方法中会处理parent和child的设置,确保在yield和恢复时能正确地在Continuation层级间导航。
pinning (固定):
Pinned枚举(NATIVE,MONITOR,CRITICAL_SECTION,EXCEPTION)定义了Continuation可能被“固定”在载体线程上而无法安全yield的原因。例如,如果Continuation的执行进入了本地方法(JNI),或者持有一个对象监视器锁(synchronized块),它就可能被固定。VThreadContinuation(在VirtualThread.java中定义) 的onPinned()方法是一个回调,当Continuation被固定时会被调用。虚拟线程的实现会根据这个信息决定是阻塞载体线程还是采取其他策略。
与 JVM 的深度集成:
@IntrinsicCandidate注解的本地方法如doYield()和enterSpecial()表明这些方法的实现是由 JVM 高度优化的,它们直接操纵线程栈和执行状态。jdk.internal.Access.JavaLangAccess(JLA) 和jdk.internal.access.SharedSecrets用于在java.base模块内部访问java.lang.Thread等类的包私有或私有成员,这是实现Continuation与线程状态紧密集成的关键。
Continuation 提供的核心能力: 它提供了一种机制,使得一段Java代码的执行可以在不阻塞底层平台线程的情况下被暂停,其状态(主要是调用栈)被保存起来,之后可以在相同的或不同的平台线程上从暂停点恢复执行。这是实现用户态线程(如虚拟线程)的基础。
实现多个虚拟线程的 JVM 级别调度还需要什么?
仅仅有 Continuation 是不够的,还需要一个完整的框架来管理和调度它们,这正是 java.lang.VirtualThread 所做的事情。关键组件包括:
虚拟线程的表示 (VirtualThread 类):
- 每个虚拟线程实例内部包装一个
Continuation。 - 管理虚拟线程的生命周期状态(
NEW,STARTED,RUNNING,PARKED,TERMINATED等)。 - 处理中断、加入 (
join) 等线程操作。
调度器 (Executor):
VirtualThread需要一个调度器(通常是ForkJoinPool,如DEFAULT_SCHEDULER)来执行它的Continuation。- 调度器负责将准备好运行的虚拟线程(其
Continuation)提交给一个可用的平台线程(载体线程)来执行。 - 当虚拟线程
yield时,它会从载体线程上卸载,载体线程可以被调度器用于执行其他虚拟线程或任务。
阻塞操作的适配:
- 标准库中的阻塞操作(如
LockSupport.park(),Object.wait(), 大部分同步 I/O 操作)需要被适配,以便在虚拟线程中调用时,它们能够触发Continuation.yield()而不是阻塞载体线程。 - 例如,
VirtualThread.park()方法会尝试yieldContinuation()。如果成功,虚拟线程挂起,载体线程释放。如果失败(因为Continuation被固定),则会退化为在载体线程上实际park。
与平台线程的交互(挂载/卸载):
VirtualThread.mount(): 当虚拟线程的Continuation开始在载体线程上运行时,需要将虚拟线程设置为Thread.currentThread()的返回值,并记录载体线程。VirtualThread.unmount(): 当Contphpinuationyield或执行完毕时,需要从载体线程上卸载,恢复Thread.currentThread()指向载体线程本身。
固定 (Pinning) 的处理:
- 需要有机制检测
Continuation何时被固定(例如,在 JNI 调用中或持有监视器锁)。 - 当虚拟线程被固定时,如果它执行了阻塞操作,那么载体线程本身可能会被阻塞,因为
Continuation无法yield。这是虚拟线程的一个重要性能考量点。
线程局部变量和作用域值:
- 需要确保线程局部变量(
ThreadLocal)对于虚拟线程按预期工作(即每个虚拟线程有自己的副本)。 ScopedValue是一个更现代的替代方案,与虚拟线程和Continuation结合得更好。Continuation类中的scopedValueCache字段就是为此服务的。
如果自己设计虚拟线程调度应该怎么做?
这是一个非常复杂的系统工程,深度依赖于 JVM 的底层支持。但从概念上讲,可以设想以下组件:
MyContinuation:
- 核心: 能够保存和恢复执行上下文(调用栈、程序计数器、寄存器)。这部分是最难的,需要 JVM 指令集层面的支持,或者像
libcontext这样的库(但 Java 没有直接使用这个)。在 JDK 中,这是通过StackChunk和大量本地代码实现的。 - 接口:
void init(Runnable task),boolean resume(),void yield(),boolean isDone()。 - 状态:
INITIAL,SUSPENDED,RUNNING,DONE。
MyVirtualThread:
- 属性: ID, 名称, 状态 (
NEW,RUNNABLE,BLOCKED,TERMINATED), 优先级 (可选)。 - 包含: 一个
MyContinuation实例,一个Runnable任务。 - 方法:
start(),interrupt(),join(),getState()。
MyScheduler (调度器):
- 核心: 一个或多个平台线程(称为工作线程或载体线程)。
- 队列: 一个或多个用于存放
MyVirtualThread的就绪队列。
调度循环:
- 工作线程从就绪队列中取出一个
MyVirtualThread。 - 设置
Thread.currentThread()指向这个MyVirtualThread(逻辑上的)。 - 调用
myVirtualThread.getContinuation().resume()。 - 如果
resume()返回是因为yield()被调用: - 根据
yield的原因(例如,等待 I/O,等待锁),将MyVirtualThread放入相应的等待结构中,或者如果只是普通的yield,放回就绪队列。 - 如果
resume()返回是因为任务完成 (isDone()为 true): - 更新
MyVirtualThread状态为TERMINATED,处理join等待。 - 恢复
Thread.currentThread()指向平台工作线程。 - 工作线程继续从队列取下一个任务。
同步原语和 I/O 适配:
锁 (MyLock): 当 MyVirtualThread 尝试获取一个已被持有的锁时,它不应阻塞平台工作线程。而是:
- 将
MyVirtualThread放入该锁的等待队列。 - 调用其
MyContinuation.yield()。 - 当锁被释放时,调度器将等待队列中的一个
MyVirtualThread移回就绪队列。
I/O: 对于非阻塞 I/O (NIO):
- 发起非阻塞 I/O 操作。
- 将
MyVirtualThread和一个回调(当 I/O 完成时调用)注册到Selector。 - 调用
MyContinuation.yield()。 - 当
Selector检测到 I/O 事件完成时,执行回调,回调将对应的MyVirtualThread重新放入调度器的就绪队列。
Pinning 处理:
- 需要一种方法来标记代码段(如 JNI 调用,
synchronized块)是不可yield的。 - 如果一个
MyVirtualThread在这种不可yield的代码段中尝试执行一个会yield的操作(如获取MyLock),调度器可能不得不阻塞当前平台工作线程,或者抛出异常,或者有其他备用策略。
挑战:
- 栈操作: 安全、高效地保存和恢复调用栈是最大的挑战,这需要 JVM 的深度配合。
- 与现有 Java 生态的兼容性: 大量现有库依赖于
Thread.currentThread()的行为和阻塞原语。 - 调试和监控: 调试跨越多个
Continuation片段的代码会更复杂。
JDK 中的 Continuation 和 VirtualThread 在 JVM 层面解决了这些核心挑战。自己从头设计一个类似的系统将是一项艰巨的任务,但理解其基本原理有助于更好地使用这些高级并发特性。
总结
以上为个人经验,希望能给大家一个参考,也希望大家多多支持编程客栈(www.devze.com)。
加载中,请稍侯......
精彩评论