🚀 优质资源分享 🚀
学习路线指引(点击解锁) | 知识定位 | 人群定位 |
---|---|---|
🧡 Python实战微信订餐小程序 🧡 | 进阶级 | 本课程是python flask+微信小程序的完美结合,从项目搭建到腾讯云部署上线,打造一个全栈订餐系统。 |
💛Python量化交易实战💛 | 入门级 | 手把手带你打造一个易扩展、更安全、效率更高的量化交易系统 |
前提
JDK19
于2022-09-20
发布GA
版本,该版本提供了虚拟线程的预览功能。下载JDK19
之后翻看了一下有关虚拟线程的一些源码,跟早些时候的Loom
项目构建版本基本并没有很大出入,也跟第三方JDK
如鹅厂的Kona
虚拟线程实现方式基本一致,这里分析一下虚拟线程设计与源码实现。
Platform Thread与Virtual Thread
因为引入了虚拟线程,原来JDK
存在java.lang.Thread
类,俗称线程,为了更好地区分虚拟线程和原有的线程类,引入了一个全新类java.lang.VirtualThread
(Thread
类的一个子类型),直译过来就是"虚拟线程"。
- 题外话:在
Loom
项目早期规划里面,核心API
其实命名为Fiber
,直译过来就是"纤程"或者"协程",后来成为了废案,在一些历史提交的Test
类或者文档中还能看到类似于下面的代码:
// java.lang.Fiber
Fiber f = Fiber.execute({
out.println("Good morning");
readLock.lock();
try{
out.println("Good night");
} finally{
readLock.unlock();
}
out.println("Good night");
});
Thread
在此基础上做了不少兼容性工作。此外,还应用了建造者模式引入了线程建造器,提供了静态工厂方法Thread#ofPlatform()
和Thread#ofVirtual()
分别用于实例化Thread
(工厂)建造器和VirtualThread
(工厂)建造器,顾名思义,两种建造器分别用于创建Thread
或者VirtualThread
,例如:
// demo-1 build platform thread
Thread platformThread = Thread.ofPlatform().daemon().name("worker").unstarted(runnable);
// demo-2 create platform thread factory
ThreadFactory platformThreadFactory = Thread.ofPlatform().daemon().name("worker-", 0).factory();
// demo-3 build virtual thread
Thread virtualThread = Thread.ofVirtual().name("virtual-worker").unstarted(runnable);
// demo-4 create virtual thread factory
ThreadFactory virtualThreadFactory = Thread.ofVirtual().name("virtual-worker-", 0).factory();
更新的JDK
文档中也把原来的Thread
称为Platform Thread
,可以更明晰地与Virtual Thread
区分开来。这里Platform Thread
直译为"平台线程",其实就是"虚拟线程"出现之前的老生常谈的"线程"。
后文会把Platform Thread称为平台线程,Virtual Thread称为虚拟线程,或者直接用其英文名称
那么平台线程与虚拟线程的联系和区别是什么?JDK
中的每个java.lang.Thread
实例也就是每个平台线程实例都在底层操作系统线程上运行Java
代码,并且平台线程在运行代码的整个生命周期内捕获系统线程。可以得出一个结论,平台线程与底层系统线程是一一对应的,平台线程实例本质是由系统内核的线程调度程序进行调度,并且平台线程的总数量受限于系统线程的总数量。
总的来说,平台线程有下面的一些特点或者说限制:
- 资源有限导致系统线程总量有限,进而导致与系统线程一一对应的平台线程有限
- 平台线程的调度依赖于系统的线程调度程序,当平台线程创建过多,会消耗大量资源用于处理线程上下文切换
- 每个平台线程都会开辟一块私有的栈空间,大量平台线程会占据大量内存
这些限制导致开发者不能极大量地创建平台线程,为了满足性能需要,需要引入池化技术、添加任务队列构建消费者-生产者模式等方案去让平台线程适配多变的现实场景。显然,开发者们迫切需要一种轻量级线程实现,刚好可以弥补上面提到的平台线程的限制,这种轻量级线程可以满足:
- 可以大量创建,例如十万级别、百万级别,而不会占据大量内存
- 由
JVM
进行调度和状态切换,并且与系统线程"松绑" - 用法与原来平台线程差不多,或者说尽量兼容平台线程现存的
API
Loom
项目中开发的虚拟线程就是为了解决这个问题,看起来它的运行示意图如下:
当然,平台线程不是简单地与虚拟线程进行1:N
的绑定,后面的章节会深入分析虚拟线程的运行原理。
虚拟线程实现原理
虚拟线程是一种轻量级(用户模式)线程,这种线程是由Java
虚拟机调度,而不是操作系统。虚拟线程占用空间小,任务切换开销几乎可以忽略不计,因此可以极大量地创建和使用。总体来看,虚拟线程实现如下:
virtual thread = continuation + scheduler
虚拟线程会把任务(一般是java.lang.Runnable
)包装到一个Continuation
实例中:
- 当任务需要阻塞挂起的时候,会调用
Continuation
的yield
操作进行阻塞 - 当任务需要解除阻塞继续执行的时候,
Continuation
会被继续执行
Scheduler
也就是执行器,会把任务提交到一个载体线程池中执行:
- 执行器是
java.util.concurrent.Executor
的子类 - 虚拟线程框架提供了一个默认的
ForkJoinPool
用于执行虚拟线程任务
下文会把carrier thread称为"载体线程",指的是负责执行虚拟线程中任务的平台线程,或者说运行虚拟线程的平台线程称为它的载体线程
操作系统调度系统线程,而Java
平台线程与系统线程一一映射,所以平台线程被操作系统调度,但是虚拟线程是由JVM
调度。JVM
把虚拟线程分配给平台线程的操作称为mount
(挂载),反过来取消分配平台线程的操作称为unmount
(卸载):
mount
操作:虚拟线程挂载到平台线程,虚拟线程中包装的Continuation
栈数据帧或者引用栈数据会被拷贝到平台线程的线程栈,这是一个从堆复制到栈的过程unmount
操作:虚拟线程从平台线程卸载,大多数虚拟线程中包装的Continuation
栈数据帧会留在堆内存中
这个mount -> run -> unmount
过程用伪代码表示如下:
mount();
try {
Continuation.run();
} finally {
unmount();
}
从Java
代码的角度来看,虚拟线程和它的载体线程暂时共享一个OS
线程实例这个事实是不可见,因为虚拟线程的堆栈跟踪和线程本地变量与平台线程是完全隔离的。JDK
中专门是用了一个FIFO
模式的ForkJoinPool
作为虚拟线程的调度程序,从这个调度程序看虚拟线程任务的执行流程大致如下:
- 调度器(线程池)中的平台线程等待处理任务
- 一个虚拟线程被分配平台线程,该平台线程作为运载线程执行虚拟线程中的任务
- 虚拟线程运行其
Continuation
,从而执行基于Runnable
包装的用户任务
- 虚拟线程任务执行完成,标记
Continuation
终结,标记虚拟线程为终结状态,清空一些上下文变量,运载线程"返还"到调度器(线程池)中作为平台线程等待处理下一个任务
上面是描述一般的虚拟线程任务执行情况,在执行任务时候首次调用Continuation#run()
获取锁(ReentrantLock
)的时候会触发Continuation
的yield
操作让出控制权,等待虚拟线程重新分配运载线程并且执行,见下面的代码:
public class VirtualThreadLock {
public static void main(String[] args) throws Exception {
ReentrantLock lock = new ReentrantLock();
Thread.startVirtualThread(() -> {
lock.lock(); // <------ 这里确保锁已经被另一个虚拟线程持有
});
Thread.sleep(1000);
Thread.startVirtualThread(() -> {
System.out.println("first");
lock.lock();
try {
System.out.println("second");
} finally {
lock.unlock();
}
System.out.println("third");
});
Thread.sleep(Long.MAX_VALUE);
}
}
- 虚拟线程中任务执行时候首次调用
Continuation#run()
执行了部分任务代码,然后尝试获取锁,会导致Continuation
的yield
操作让出控制权(任务切换),也就是unmount
,运载线程栈数据会移动到Continuation
栈的数据帧中,保存在堆内存,虚拟线程任务完成(但是虚拟线程没有终结,同时其Continuation
也没有终结和释放),运载线程被释放到执行器中等待新的任务;如果Continuation
的yield
操作失败,则会对运载线程进行park
调用,阻塞在运载线程上
- 当锁持有者释放锁之后,会唤醒虚拟线程获取锁(成功后),虚拟线程会重新进行
mount
,让虚拟线程任务再次执行,有可能是分配到另一个运载线程中执行,Continuation
栈会的数据帧会被恢复到运载线程栈中,然后再次调用Continuation#run()
恢复任务执行:
- 最终虚拟线程任务执行完成,标记
Continuation
终结,标记虚拟线程为终结状态,清空一些上下文变量,运载线程"返还"到调度器(线程池)中作为平台线程等待处理下一个任务
Continuation
组件十分重要,它既是用户真实任务的包装器,也是任务切换虚拟线程与平台线程之间数据转移的一个句柄,它提供的yield
操作可以实现任务上下文的中断和恢复。由于Continuation
被封闭在java.base/jdk.internal.vm
下,可以通过增加编译参数--add-exports java.base/jdk.internal.vm=ALL-UNNAMED
暴露对应的功能,从而编写实验性案例,IDEA
中可以按下图进行编译参数添加:
然后编写和运行下面的例子:
import jdk.internal.vm.Continuation;
import jdk.internal.vm.ContinuationScope;
public class ContinuationDemo {
public static void main(String[] args) {
ContinuationScope scope = new ContinuationScope("scope");
Continuation continuation = new Continuation(scope, () -> {
System.out.println("Running before yield");
Continuation.yield(scope);
System.out.println("Running after yield");
});
System.out.println("First run");
// 第一次执行Continuation.run
continuation.run();
System.out.println("Second run");
// 第二次执行Continuation.run
continuation.run();
System.out.println("Done");
}
}
// 运行代码,神奇的结果出现了
First run
Running before yield
Second run
Running after yield
Done
这里可以看出Continuation
的奇妙之处,Continuation
实例进行yield
调用后,再次调用其run
方法就可以从yield
的调用之处往下执行,从而实现了程序的中断和恢复。
源码分析
主要包括:
Continuation
VirtualThread
- 线程建造器
Continuation
Continuation
直译为"连续",一般来说表示一种语言构造,使语言可以在任意点保存执行状态并且在之后的某个点返回。在JDK
中对应类jdk.internal.vm.Continuation
,这个类只有一句类注释A one-shot delimited continuation
,直译为一个只能执行一次的回调函数。由于Continuation
的成员和方法缺少详细的注释,并且大部分功能由JVM
实现,这里只能阅读其一些骨干源码和上一小节编写的Continuation
相关例子去了解其实现(笔者C
语言比较薄弱,有兴趣的可以翻阅JVM
的源码)。先看成员变量和构造函数:
// 判断是否需要保留当前线程的本地缓存,由系统参数jdk.preserveExtentLocalCache决定
private static final boolean PRESERVE_EXTENT_LOCAL_CACHE;
// 真正要被执行的任务实例
private final Runnable target;
// 标识Continuation的范围,
private final ContinuationScope scope;
// Continuation的父节点,如果为空的时候则为本地线程栈
private Continuation parent;
// Continuation的子节点,非空时候说明在子Continuation中进行了yield操作
private Continuation child;
// 猜测为Continuation栈结构,由JVM管理,无法得知其真实作用
private StackChunk tail;
// 标记Continuation是否已经完成
private boolean done;
// 标记是否进行了mount操作
private volatile boolean mounted = false;
// yield操作时候设置的信息
private Object yieldInfo;
// 标记一个未挂载的Continuation是否通过强制抢占式卸载
private boolean preempted;
// 保留当前线程的本地缓存的副本
private Object[] extentLocalCache;
// 构造函数,要求传入范围和任务包装实例
public Continuation(ContinuationScope scope, Runnable target) {
this.scope = scope;
this.target = target;
}
Continuation
是一个双向链表设计,它的唯一一组构造参数是ContinuationScope
和Runnable
: