引言 线程资源
线程资源的调度和处理是一门大学问,且有比较明显的演进过程。
- 线程:408基础,不赘述了。创建销毁,切换线程的代价都较大。
- 线程池:线程复用解决创建销毁开销,切换线程还是未解决。
- 异步编程:切换线程的一般是因为阻塞,如果采用异步编程去手动充分利用线程资源防止阻塞,那也就变相改善了切换线程的现象。但是代价是明显的,需要复杂的异步编程,对每个资源都要梳理清楚。
- 虚拟线程:时间片资源耗尽导致的线程切换无法逾越,但当任务等待 I/O 或其他支持虚拟线程的阻塞操作时,是可以管理的。虚拟线程直接自己管理IO或其他阻塞导致的线程切换。
虚拟线程
资源阻塞导致的线程上下文切换,被JVM当成业务的一部分被处理。
其实大致的实现思路很容易想到,可以考虑为实现一个调度器和一个虚拟线程队列,一个存虚拟线程跑在哪被暂停的状态流转类,当然还有本来就必须的真实线程。但是问题要融入jdk本身的框架,还是比较绕的。
核心角色
- VirtualThread 虚拟线程
- Continuation 状态流转
- ForkJoinPool 调度器,维护虚拟线程队列和CarrierThread
- CarrierThread 真实资源平台线程
运行架构
必须牢记虚拟线程是一个“业务”概念,不然容易弄混。
用线程视角可以这样分
主线程A,主线程用来触发虚拟线程的
- 驱动虚拟线程C,给虚拟线程包装好。
thread.start()后,将包装了 Continuation 的runContinuation提交到 ForkJoinPool。- 执行 ForkJoinPool 的入队和 worker 唤醒逻辑。
- 主线程的任务就算完成了
线程B,也就是CarrierThread
- 循环驱动从ForkJoinPool消费虚拟线程队列。
连起来就是
线程 A 调用 virtualThread.start()
↓
线程 A 执行 submitRunContinuation()
↓
线程 A 调用 scheduler.execute(runContinuation)
↓
线程 A 将任务压入 ForkJoinPool 的工作队列/提交队列,并触发 worker
↓
某个 CarrierThread 取出 runContinuation
↓
CarrierThread 执行 cont.run(),虚拟线程在这里完成 mount
有点类似mq的感觉,将待执行的交由一个队列,然后由另一个消费。
VirtualThread
A thread that is scheduled by the Java virtual machine rather than the operating system.
首先为了不破坏Thread,VirtualThread(vt)使用方法和Thread一致,甚至vt 的类的访问修饰符缺省,也就是只能同包访问,只能通过Thread的api拿到vt对象。两个主要api获取vt:
Thread thread = Thread.startVirtualThread(() -> doWork());
Thread thread = Thread.ofVirtual().start(() -> doWork()); 来点核心源码,构造器
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
scheduler = vparent.scheduler;
} else {
scheduler = DEFAULT_SCHEDULER; // ForkJoinPool,默认实现
}
}
this.scheduler = scheduler; // 调度器
this.cont = new VThreadContinuation(this, task); // Continuation,状态流转包装类
this.runContinuation = this::runContinuation; // 核心 Runnable lambda表达式将方法传入,原始task使用cont包装一下。等真实线程carrier利用ForkJoinPool调用。
}this::runContinuation核心源码
mount(); // 挂载
try {
cont.run(); // 启动
} finally {
unmount(); // 卸载
if (cont.isDone()) {
afterDone(); // 做完了
} else {
afterYield(); // 没做完
}
}拓展:Go 协程
虚拟线程可以理解为 Java 在保留原有 Thread 编程模型的前提下,把操作系统线程之上的任务调度能力收回到 JVM;goroutine 则是 Go 从语言、运行时到并发工具整体设计出来的轻量级执行模型。
其实核心就是线程和调度器的互动关系是怎么建模的。两边的调度器都不应该被理解成一个独立运行的“调度线程”。调度器本质上是一套数据结构和调度算法:
- 创建任务的线程负责执行入队、唤醒工作线程等逻辑
- 工作线程负责寻找任务、运行任务以及重新进入调度循环
- 网络轮询器、计时器等运行时组件负责在条件满足后唤醒任务
调度模型
Go 调度器则通常使用 G、M、P 三个概念描述:
- G(Goroutine):待执行的 goroutine,和 VirtualThread 比较接近
- M(Machine):操作系统线程,和 CarrierThread 比较接近
- P(Processor):执行 Go 代码所需要的调度资源,并持有本地运行队列;M 必须关联 P 才能执行普通 Go 代码
抢占方式
Go runtime 支持抢占长时间运行的 goroutine。即使某个 goroutine 没有主动阻塞,运行时也可以让它暂时停止,使其他 goroutine 获得执行机会。抢占时机发生在允许运行时安全暂停 goroutine 的安全点。