JDK19虚拟线程初探(二)

简介

在上一篇文章中,我们已经简单的介绍了JEP 425: Virtual Threads (Preview),以及如何使用虚拟线程编写例程,本文中将继续介绍虚拟线程的底层机制和核心源码。

虚拟线程的机制

调度模型

与Golang协程调度的GPM模型类似,JDK19中的虚拟线程也涉及类似的定义:

  • VT:虚拟线程
  • Platform Thread:平台线程,一个平台线程上可以运行很多虚拟线程
  • OS Thread:操作系统线程,hotspot线程模型中,平台线程和OS线程关系为1:1。
    JDK19虚拟线程初探(二)_第1张图片

平台线程和虚拟线程

除了早期的Green Thread方案,JVM中平台线程和OS线程都是1:1关系,因此平台线程限制颇多:

  • 创建新线程需要调用pthread API,代价高昂。
  • 受限于操作系统资源,平台线程数量不能过多。
  • 线程过多时,大量CPU时间浪费在线程上下文切换,sys占用升高。
  • 线程栈占用大量内存。
    通过资源池,可以减少创建和销毁线程的代价,但上下文切换、数量限制和内存占用在平台线程框架内无法解决。

相比而言,虚拟线程优势相当明显:

  • 虚拟线程仅在用户态即可创建,可以轻松创建上百万个。
  • 虚拟线程的调度和切换,仅在用户态即可完成,没有内核上下文切换的开销。
  • 虚拟线程栈内存占用小。

另一方面,虚拟线程也不适应如下场景:

  • 阻塞操作时,如果VT没有主动释放执行权限,将阻塞整个平台线程。
  • 计算机密集型,同样的如果没有主动触发调度,将会导致其他VT饿死。

核心代码

为了兼容现有的Java线程API体系,Thread新建了子类BaseVirtualThread、VirtualThread,帮助VT完全适配当前的JUC线程体系。
JDK19虚拟线程初探(二)_第2张图片

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;

JDK19虚拟线程初探(二)_第3张图片

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线程体系,对下一步常见的后端框架迁移虚拟线程打下了良好的基础。

引用

JEP 425

你可能感兴趣的:(JVM,java,jvm,开发语言)