JDK21 VirtualThread 初识原理入门

449 阅读8分钟

Java Virtual Thread

环境:JDK21

推荐阅读

image.png

虚拟线程是轻量级线程(类似于 Go 中的 “协程(Goroutine)”),可以减少编写、维护和调度高吞吐量并发应用程序的工作量。

线程是可供调度的最小处理单元,它与其他类似的处理单元并发运行,并且在很大程度上是独立运行的。线程(java.lang.Thread)有两种,平台线程和虚拟线程。

平台线程

平台线程也就是之前的普通线程 java.lang.Thread 的实例,它被实现为对操作系统线程的简单包装,它通常以 1:1 的比例映射到由操作系统调度的内核线程中。它在其底层操作系统线程上运行 Java 代码,并且在它的整个生命周期内捕获着其映射的操作系统线程。因此,可用平台线程的数量局限于对应操作系统线程的数量。

平台线程通常有一个大的堆栈和其他由操作系统维护的资源,它适合运行所有类型的任务,但可供使用的资源可能有限。

平台线程可被指定为守护线程或非守护线程,除了守护线程状态之外,平台线程还具有线程优先级,并且是线程组的成员。默认情况下,平台线程会获得自动生成的线程名称。

与此同时,关于线程还有一些需要特别提到的变更,并值得注意:如果先前有通过直接 new Thread(...) 手工创建单个平台线程并使用(尽管此做法在大多数情况下是不推荐的)的话,请记住 Java 21 中的 suspend()、 resume()、stop() 和 countStackFrames() 等弃用方法将会直接抛出 UnsupportedOperationException 异常,可能会影响到之前的业务处理逻辑!

虚拟线程

与平台线程一样,虚拟线程同样是 java.lang.Thread 的实例,但是,虚拟线程并不与特定的操作系统线程绑定。它与操作系统线程的映射关系比例也不是 1:1,而是 m:n。虚拟线程通常是由 Java 运行时来调度的,而不是操作系统。虚拟线程仍然是在操作系统线程上运行 Java 代码,但是,当在虚拟线程中运行的代码调用阻塞的 I/O 操作时,Java 运行时会将虚拟线程挂起,直到其可以恢复为止。此时与挂起的虚拟线程相关联的操作系统线程便可以自由地为其他虚拟线程来执行操作。

与平台线程不同,虚拟线程通常有一个浅层调用栈,它只需要很少的资源,单个 Java 虚拟机可能支持数百万个虚拟线程(也正因为如此,尽管虚拟线程支持使用 ThreadLocal 或 InheritableThreadLocal 等线程局部变量,也应该仔细考虑是否需要使用它们)。虚拟线程适合执行大部分时间被阻塞的任务,这些任务通常需要等待 I/O 操作完成,它不适合用于长时间运行的 CPU 密集型操作。

虚拟线程通常使用一小组平台线程作为载体线程(Carrier Thread),在虚拟线程中运行的代码不知道其底层的载体线程。

虚拟线程是守护线程,具有固定的线程优先级,不能更改。默认情况下,虚拟线程没有线程名称,如果未设置线程名称,则获取当前线程名称时将会返回空字符串。

那么,为什么要使用虚拟线程呢?

在高吞吐量并发应用程序中使用虚拟线程,尤其是那些包含由大量并发任务组成的应用程序,这些任务需要花费大量时间等待。例如服务器应用程序,因为它们通常处理许多执行阻塞 I/O 操作(例如获取资源)的客户端请求。

虚拟线程并不是更快的线程,它们运行代码的速度并不会比平台线程更快。它们的存在是为了提高扩展性(更高的吞吐量,而吞吐量意味着系统在给定时间内可以处理多少个信息单元),而不是速度(更低的延迟)。

创建和运行虚拟线程

  1. Thread.ofVirtual() 创建和运行虚拟线程

    Thread thread = Thread.ofVirtual()
                            .start(() -> System.out.println("Hello"));
    thread.join();   // 等待虚拟线程终止
    

    Thread.startVirtualThread(task) 可以快捷地创建并启动虚拟线程,它与 Thread.ofVirtual().start(task) 是等价的。

  2. Thread.Builder 创建和运行虚拟线程

    Thread.Builder 接口允许创建具有通用的线程属性(例如线程名称)的线程,Thread.Builder.OfPlatform 子接口创建平台线程,而 Thread.Builder.OfVirtual 子接口则创建虚拟线程。

    Thread.Builder builder = Thread.ofVirtual().name("MyThread");  // 虚拟线程的名称是 MyThread
    Runnable task = () -> System.out.println("Running thread");
    Thread t = builder.start(task);
    System.out.println("Thread t name: " + t.getName());      // 控制台打印:Thread t name: MyThread
    t.join(); 
    
  3. Executors.newVirtualThreadPerTaskExecutor() 创建和运行虚拟线程

    Executor 允许将线程管理和创建与应用程序的其余部分分开:

    // Java 21 中 ExecutorService 接口继承了 AutoCloseable 接口,
    // 所以可以使用 try-with-resources 语法使 Executor 在最后被自动地 close()
    try (ExecutorService myExecutor = Executors.newVirtualThreadPerTaskExecutor()) {
        // 每次 submit() 调用向 Executor 提交任务时都会创建和启动一个新的虚拟线程
        Future<?> future = myExecutor.submit(() -> System.out.println("Running thread"));
        future.get();  // 等待线程任务执行完成
        System.out.println("Task completed");
    } catch (ExecutionException | InterruptedException ignore) {}
    

虚拟线程采用指南

虚拟线程是由 Java 运行时而不是操作系统实现的 Java 线程。虚拟线程和传统线程(现在称之为平台线程)之间的主要区别在于,可以很容易地在同一个 Java 进程中运行大量活动的虚拟线程,甚至数百万个。大量的虚拟线程赋予了它们强大的功能:通过允许服务器并发处理更多的请求,它们可以更有效地运行以每个请求一个线程的方式编写的服务器应用程序,从而实现更高的吞吐量和更少的硬件浪费。

由于虚拟线程是 java.lang.Thread 的实现,并且遵循自 Java SE 1.0 以来指定的 java.lang.Thread 的相同规则,因此开发人员不需要学习使用它们的新概念。然而,由于无法生成非常多的平台线程(多年来 Java 中唯一可用的线程实现),因此产生了旨在应对其高成本的实践做法。当这些做法应用于虚拟线程时会适得其反,必须摒弃。此外,成本上的巨大差异提示了一种考虑线程的新方式,这些线程一开始可能是外来的。

  1. 编写简单、同步的代码,采用单请求单线程风格的阻塞 I/O API
  2. 将每个并发任务表示为一个虚拟线程,不要池化虚拟线程
  3. 使用信号量限制并发
  4. 不要在线程局部变量中缓存昂贵的可重用对象
  5. 避免长时间和频繁的固定
    • 目前虚拟线程的实现存在一个限制,即在 synchronized 同步块或方法内执行阻塞操作会导致 JDK 的虚拟线程调度器阻塞一个宝贵的操作系统线程
    • 使用 ReentrantLock 替代 synchronized

生命周期

virtual-thread-state.svg

实现原理

virtual thread = continuation + scheduler

虚拟线程会把任务(一般是java.lang.Runnable)包装到一个Continuation实例中:

  • 当任务需要阻塞挂起的时候,会调用Continuation的yield操作进行阻塞
  • 当任务需要解除阻塞继续执行的时候,Continuation会被继续执行

Scheduler也就是执行器,会把任务提交到一个载体线程池中执行:

  • 执行器是java.util.concurrent.Executor的子类
  • 虚拟线程框架提供了一个默认的ForkJoinPool用于执行虚拟线程任务

VirtualThread.start

    @Override
    void start(ThreadContainer container) {
        if (!compareAndSetState(NEW, STARTED)) {
            throw new IllegalThreadStateException("Already started");
        }

        // bind thread to container
        assert threadContainer() == null;
        setThreadContainer(container);

        // start thread
        boolean addedToContainer = false;
        boolean started = false;
        try {
            container.onStart(this);  // may throw
            addedToContainer = true;

            // scoped values may be inherited
            inheritScopedValueBindings(container);

            // submit task to run thread
            // 执行
            submitRunContinuation();
            started = true;
        } finally {
            if (!started) {
                afterDone(addedToContainer);
            }
        }
    }
    private void submitRunContinuation() {
        try {
            //扔给线程池
            scheduler.execute(runContinuation);
        } catch (RejectedExecutionException ree) {
            submitFailed(ree);
            throw ree;
        }
    }

VirtualThread默认线程池是一个ForkJoinPool

源码分析

BaseVirtualThread

sealed abstract class BaseVirtualThread extends Thread
        permits VirtualThread, ThreadBuilders.BoundVirtualThread {

    BaseVirtualThread(String name, int characteristics, boolean bound) 
    {
        super(name, characteristics, bound);
    }

    abstract void park();

    abstract void parkNanos(long nanos);

    abstract void unpark();
}


BaseVirtualThread 是一个密封类,继承自 Thread ,被 VirtualThreadThreadBuilders.BoundVirtualThread 继承。

  • VirtualThread是Java正宗的虚拟线程
  • BoundVirtualThread是在Jvm不支持虚拟线程时的产物,实现方式和PlatformThread差不多
    static Thread newVirtualThread(Executor scheduler,
                                   String name,
                                   int characteristics,
                                   Runnable task) {
        if (ContinuationSupport.isSupported()) {
            return new VirtualThread(scheduler, name, characteristics, task);
        } else {
            if (scheduler != null)
                throw new UnsupportedOperationException();
            return new BoundVirtualThread(name, characteristics, task);
        }
    }

下文主要分析VirtualThread

BaseVirtualThread新增了三个方法,都是与park相关的,因为VirtualThread的park和LockSupport对PlatformThread的park有很大的不同

PlatformThread park

先看一下LockSupport是怎么park的

    public static void park(Object blocker) {
        Thread t = Thread.currentThread();
        setBlocker(t, blocker);
        try {
            if (t.isVirtual()) {
                VirtualThreads.park();
            } else {
                U.park(false, 0L);
            }
        } finally {
            setBlocker(t, null);
        }
    }

如果是PlatformThread直接调用Unsafe.park

LockSupport.park -> Unsafe.park -> JNI(JVM) -> Parker::park -> pthread_cond_wait -> futex -> sys_futex(系统调用)

调用后线程阻塞

VirtualThread park

VirtualThread 的调度基于协作式调度(Cooperative Scheduling),即虚拟线程在等待 I/O 或达到某些预定义的任务完成点时,主动将控制权交还给 JVM 的调度器,从而让其他虚拟线程获得执行机会

    @Override
    void park() {
        assert Thread.currentThread() == this;

        // complete immediately if parking permit available or interrupted
        if (getAndSetParkPermit(false) || interrupted)
            return;

        // park the thread
        boolean yielded = false;
        setState(PARKING);
        try {
            yielded = yieldContinuation();  // may throw
        } finally {
            assert (Thread.currentThread() == this) && (yielded == (state() == RUNNING));
            if (!yielded) {
                assert state() == PARKING;
                setState(RUNNING);
            }
        }

        // park on the carrier thread when pinned
        if (!yielded) {
            parkOnCarrierThread(false, 0);
        }
    }

VirtualThread park 会先让当前线程yield,将cpu让给其他线程。 如果yield之后还是自己有执行权才执行Unsafe.park