简介
在上一篇文章中,我们已经简单的介绍了JEP 425: Virtual Threads (Preview),以及如何使用虚拟线程编写例程,本文中将继续介绍虚拟线程的底层机制和核心源码。
虚拟线程的机制
调度模型
与Golang协程调度的GPM模型类似,JDK19中的虚拟线程也涉及类似的定义:
- VT:虚拟线程
- Platform Thread:平台线程,一个平台线程上可以运行很多虚拟线程
- OS Thread:操作系统线程,hotspot线程模型中,平台线程和OS线程关系为1:1。
平台线程和虚拟线程
除了早期的Green Thread方案,JVM中平台线程和OS线程都是1:1关系,因此平台线程限制颇多:
- 创建新线程需要调用pthread API,代价高昂。
- 受限于操作系统资源,平台线程数量不能过多。
- 线程过多时,大量CPU时间浪费在线程上下文切换,sys占用升高。
- 线程栈占用大量内存。
通过资源池,可以减少创建和销毁线程的代价,但上下文切换、数量限制和内存占用在平台线程框架内无法解决。
相比而言,虚拟线程优势相当明显:
- 虚拟线程仅在用户态即可创建,可以轻松创建上百万个。
- 虚拟线程的调度和切换,仅在用户态即可完成,没有内核上下文切换的开销。
- 虚拟线程栈内存占用小。
另一方面,虚拟线程也不适应如下场景:
- 阻塞操作时,如果VT没有主动释放执行权限,将阻塞整个平台线程。
- 计算机密集型,同样的如果没有主动触发调度,将会导致其他VT饿死。
核心代码
为了兼容现有的Java线程API体系,Thread新建了子类BaseVirtualThread、VirtualThread,帮助VT完全适配当前的JUC线程体系。
BaseVirtualThread
BaseVirtualThread是个抽象类,提供了三个抽象方法park、parkNanos、unpark。
sealed abstract class BaseVirtualThread extends Thread
permits VirtualThread, ThreadBuilders.BoundVirtualThread {
}
- sealed关键字是JDK15中引入的特性,定义BaseVirtualThread 为封闭类,只能被VirtualThread和 ThreadBuilders.BoundVirtualThread继承
- sealed的子类必须是final或者sealed类
VirtualThread
首先介绍VirtualThread几个核心的变量
// 需要执行的任务
private final Continuation cont;
// 执行任务包装类
private final Runnable runContinuation;
// 虚拟线程的状态
private volatile int state;
// park许可
private volatile boolean parkPermit;
// carrier线程,即虚拟线程绑定的平台线程
private volatile Thread carrierThread;
// 调度器
private final Executor scheduler;
private static final ForkJoinPool DEFAULT_SCHEDULER = createDefaultScheduler();
// 唤醒线程池
private static final ScheduledExecutorService UNPARKER = createDelayedTaskScheduler();
虚拟线程由JVM调度,JVM将VT分配给平台线程的动作称为挂载(mount),取消分配的动作称为卸载(unmount),线程状态如下:
// 初始状态
private static final int NEW = 0;
// 线程启动,由于虚拟线程的run()是个空方法,此时尚未开始执行任务
// 真正的任务执行在cont.run
private static final int STARTED = 1;
// 可执行,尚未分配平台线程
private static final int RUNNABLE = 2;
// 可执行,已分配平台线程
private static final int RUNNING = 3;
// 线程尝试park
private static final int PARKING = 4;
// 从平台线程卸载
private static final int PARKED = 5;
// cont.yield失败,未从平台线程卸载
private static final int PINNED = 6;
// 尝试cont.yield
private static final int YIELDING = 7;
// 终结态
private static final int TERMINATED = 99;
getAndSetParkPermit
先介绍一个基础方法,通过unsafe工具提供的CAS能力,申请或者释放许可。
// park许可,volatile 变量
private volatile boolean parkPermit;
// CAS修改park许可
private boolean getAndSetParkPermit(boolean newValue) {
if (parkPermit != newValue) {
return U.getAndSetBoolean(this, PARK_PERMIT, newValue);
} else {
return newValue;
}
}
parkNanos
void parkNanos(long nanos) {
// park线程指定的纳秒数
if (nanos > 0) {
long startTime = System.nanoTime();
boolean yielded;
// 提交一个nanos后unpark的任务
Future<?> unparker = scheduleUnpark(nanos);
// 修改VT状态为PARKING
setState(PARKING);
try {
// 卸载虚拟线程
// 执行continuation的yield
// continue时,重新挂载虚拟线程
yielded = yieldContinuation();
} finally {
assert (Thread.currentThread() == this)
&& (state() == RUNNING || state() == PARKING);
cancel(unparker);
}
// 如果yieldContinuations失败,重新计算时间并在平台线程上park
if (!yielded) {
long deadline = startTime + nanos;
if (deadline < 0L)
deadline = Long.MAX_VALUE;
parkOnCarrierThread(true, deadline - System.nanoTime());
}
}
}
- yieldContinuation有个有意思的注解ChangesCurrentThread,表示方法体内调用了Thread.setCurrentThread,该方法无法被内联,除非内联的方法也有ChangesCurrentThread注解。
unpark
// 方法体内调用了Thread.setCurrentThread
@ChangesCurrentThread
void unpark() {
Thread currentThread = Thread.currentThread();
// park许可修改为true,当前线程不是this
if (!getAndSetParkPermit(true) && currentThread != this) {
int s = state();
// 如果VT未挂载,则先挂载
if (s == PARKED && compareAndSetState(PARKED, RUNNABLE)) {
if (currentThread instanceof VirtualThread vthread) {
// 切换到平台线程
Thread carrier = vthread.carrierThread;
carrier.setCurrentThread(carrier);
try {
// 提交runContinuation到调度器
submitRunContinuation();
} finally {
// 再切换回虚拟线程
carrier.setCurrentThread(vthread);
}
} else {
submitRunContinuation();
}
} else if (s == PINNED) {
// 平台线程的内部对象锁,interruptLock
synchronized (carrierThreadAccessLock()) {
// unpack VT,修改线程状态为running
Thread carrier = carrierThread;
if (carrier != null && state() == PINNED) {
U.unpark(carrier);
}
}
}
}
}
- parkNanos和unpark是一对操作,parkNanos用于阻塞虚拟线程直到指定时间,提前调用unpark也可以终止阻塞,unpark用于重新启用当前虚拟线程进行调度。
sleep和doSleepNanos
了解了上面parkNanos和unpark机制,我们就看轻松理解例程中的业务代码和线程sleep是如何交替执行的了。
Thread.sleep在JDK19中被修改了:
public static void sleep(long millis) throws InterruptedException {
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (currentThread() instanceof VirtualThread vthread) {
// 如果是虚拟线程,则调用虚拟线程的sleepNanos,并最终调用doSleepNanos
long nanos = MILLISECONDS.toNanos(millis);
vthread.sleepNanos(nanos);
return;
}
// 平台线程的sleep,不看了
}
虚拟线程的doSleepNanos方法如下:
private void doSleepNanos(long nanos) throws InterruptedException {
if (nanos == 0) {
// 与平台线程类似,如果sleep 0纳秒,则调用yield释放CPU,触发重新调度
tryYield();
} else {
try {
long remainingNanos = nanos;
long startNanos = System.nanoTime();
while (remainingNanos > 0) {
// park 指定纳秒
parkNanos(remainingNanos);
// 判断中断信号
if (getAndClearInterrupt()) {
throw new InterruptedException();
}
remainingNanos = nanos - (System.nanoTime() - startNanos);
}
} finally {
// 释放park许可
setParkPermit(true);
}
}
}
结论
借助VirtualThread和BaseVirtualThread类,虚拟线程成功地适配了现有的Java线程体系,对下一步常见的后端框架迁移虚拟线程打下了良好的基础。