引言 线程资源

线程资源的调度和处理是一门大学问,且有比较明显的演进过程。

虚拟线程

资源阻塞导致的线程上下文切换,被JVM当成业务的一部分被处理。

其实大致的实现思路很容易想到,可以考虑为实现一个调度器和一个虚拟线程队列,一个存虚拟线程跑在哪被暂停的状态流转类,当然还有本来就必须的真实线程。但是问题要融入jdk本身的框架,还是比较绕的。

核心角色

运行架构

必须牢记虚拟线程是一个“业务”概念,不然容易弄混。

用线程视角可以这样分
主线程A,主线程用来触发虚拟线程的

线程B,也就是CarrierThread

连起来就是

线程 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 三个概念描述:

抢占方式

Go runtime 支持抢占长时间运行的 goroutine。即使某个 goroutine 没有主动阻塞,运行时也可以让它暂时停止,使其他 goroutine 获得执行机会。抢占时机发生在允许运行时安全暂停 goroutine 的安全点。