JDK21中的虚拟线程

虚拟线程的概念、使用、注意事项、源码概览

什么是虚拟线程

在虚拟线程(Virtual Threads)出现之前,Java中只有一个Thread(虚拟线程出来后也被叫做平台线程PlatformThread)。一般来说一个PlatformThread对应一个操作系统线程,所以平台线程的数量受限于操作系统的线程数量上限。

Java SE最常用的JVM是Oracle/Sun研发的HotSpot VM。在这个JVM的较新版本所支持的所有平台上,它都是使用1:1线程模型的——除了Solaris之外,它是个特例。

其实JVM规范里并没有规定是按1:1线程模型,但绝大多数都是1:1的。具体请参考R大的回复:对JVM中的线程模型是用户级的么?

虚拟线程在JEP5425提案中作为Preview功能在JDK19中出现,并在JDK21中以JEP444提案作为正式功能release。

JEP444详细记录了虚拟线程相关信息,非常值得阅读和参考。https://openjdk.org/jeps/444

Virtual threads are lightweight threads that dramatically reduce the effort of writing, maintaining, and observing high-throughput concurrent applications.

官方定性: 虚拟线程是能显著减少编写、维护和观察高吞吐量应用的轻量级线程。

Virtual threads are a lightweight implementation of threads that is provided by the JDK rather than the OS. They are a form of user-mode threads, which have been successful in other multithreaded languages (e.g., goroutines in Go and processes in Erlang). User-mode threads even featured as so-called “green threads” in early versions of Java, when OS threads were not yet mature and widespread. However, Java’s green threads all shared one OS thread (M:1 scheduling) and were eventually outperformed by platform threads, implemented as wrappers for OS threads (1:1 scheduling). Virtual threads employ M:N scheduling, where a large number (M) of virtual threads is scheduled to run on a smaller number (N) of OS threads.

To run code in a virtual thread, the JDK’s virtual thread scheduler assigns the virtual thread for execution on a platform thread by mounting the virtual thread on a platform thread. This makes the platform thread become the carrier of the virtual thread.

个人理解,虚拟线程和go中goroutine其实本质差不多,都是用户级轻量级线程调度(协程的概念)。在Java中,多个虚拟线程运行在同一个平台线程上,由平台线程自己调度虚拟线程的挂起和执行(M:N 调度,大量的虚拟线程(M)被调度在较少数量的操作系统线程(N)上运行)。所以平台线程也可以称作是虚拟线程的载体(carrier)。

虚拟线程

这些虚拟线程的堆栈存储在Java的垃圾收集堆上,可以被正常gc的,创建虚拟线程的开销非常小(虚拟线程的线程栈不同于平台线程,不属于gc roots)。而且因为是用户态线程管理调度,所以也不需要像传统线程一样需要在用户态(User Mode)和内核态(Kernel Mode)中来回切换,也就是不存在所谓的上下文切换。

虚拟线程的几个目标就是:

  1. 可以用最简单的thread-per-request这样的风格来编程。(不用再去写异步的方式来挺高并发和吞吐能力)
  2. 使用 java.lang.Thread API 的现有代码能够以最小的更改采用虚拟线程。(设计真的很合理,基本就是对原来Thread的扩展,平台线程支持的能力虚拟线程都支持,如局部变量、线程中断等等)
  3. 使用现有 JDK 工具轻松进行虚拟线程故障排除、调试和分析。(即使是虚拟线程,也基本可以当作是PlatformThread来定位排障)

关于虚拟线程及其使用

快速新建虚拟线程任务并执行:

Thread.startVirtualThread(() -> System.out.println("虚拟线程执行中..."));
Thread.ofVirtual().start(() -> System.out.println("虚拟线程执行中..."));
Thread.ofVirtual().unstarted(() -> System.out.println("虚拟线程执行中...")).start();

通过创建ThreadFactory创建虚拟线程

var virtualFactory = Thread.ofVirtual().factory();
virtualFactory.newThread(() -> System.out.println("虚拟线程执行中...")).start();

针对一些线程池化的业务,也可以用Executors.newVirtualThreadPerTaskExecutor()代替原有线程池。

try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    IntStream.range(0, 10_000).forEach(i -> {
        executor.submit(() -> {
            Thread.sleep(Duration.ofSeconds(1));
            return i;
        });
    });
}  // executor.close() is called implicitly, and waits

结构化并发(Structured Concurrency)相关的API也支持创建和管理虚拟线程,结构化并发也更好的编排线程之间的关系(包括虚拟线程和平台线程)。但是结构化并发相关API还处于孵化阶段,还没正式release,这里就不多介绍了。

虚拟线程使用时需要注意的点

软件工程里不存在银弹,虚拟线程在使用的过程中也遇到了一些需要注意的点。

  1. 不要池化虚拟线程
    • 虚拟线程相当的cheap,池化虚拟线程反而会导致并发起不来
  2. ExecutorService.shutdown()对于虚拟线程来说,并不是阻塞的,会立即返回。
    • 对于已提交的任务,shutDown()不会终止已提交的任务,shutDownNow()会终止。
      var executor = Executors.newVirtualThreadPerTaskExecutor();
      for (int i = 0; i < 10; i++) {
       int finalI = i;
       executor.execute(() -> {
        try {
            Thread.sleep(Duration.ofSeconds(2));
            log.info("虚拟线程替换线程池执行中..." + finalI);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
       });
      }
      // shutdownNow()和shutdown()的区别,和`ThreadPoolExecutor`的定义和返回是有区别的
      //executor.shutdownNow();
      executor.shutdown();
      Thread.currentThread().join(10 * 1000);
      
  3. 虚拟线程本身是支持ThreadLocalInheritableThreadLocal的(对载体线程完全无感,和平台线程使用感受一致),但是虚拟线程非常廉价,可能存在非常非常多的虚拟线程,所以使用ThreadLocal时要千万小心。后续Scoped values可能是代替的一个更好选择,不过目前还在孵化中。

  4. 通常情况下,虚拟线程在阻塞I/O或其他阻塞操作时,会被unmount,下面两种情况下,虚拟线程无法被卸载,会被固定到运行它的载体上,感觉这也是使用work-stealing的FokrJoinPool来调度的理由之一(Continuation可以yieldReentrantLock的lock,但是synchronized和native方法/外部函数):
    • 调用native方法或Foreign Function(外部函数)
    • 执行synchronized方法或者代码块(可以使用ReentrantLock代替,因为虚拟线程支持LockSupport的相关API)

虚拟线程的源码设计

virtual thread = Continuation(Runnable + ContinuationScope) + Scheduler(一个单独的ForkJoinPool,不是commonPool)

Continuation可以理解为是对虚拟线程要执行的任务的包装,提供了虚拟线程yield/run,mount/unmount的能力。

  • 当任务阻塞需要挂起时,会执行Continuation.yield(ContinuationScope scope)方法,虚拟线程unmount;
  • 当任务恢复时,会调用 Continuation.run 会从阻塞点之后继续执行。

Scheduler可以理解为负责调度和提供实际执行任务的载体(carrier),其本身是一个ForkJoinPool(并不是Stream并行计算时用的那个commonPool!)

实际使用虚拟线程的感受

  1. 如果你的业务本身就是异步编程时,性能上提升可能并不多。
    • 但是相对于异步编程,虚拟线程的thread-per-request风格,代码更简单易懂,符合直觉,且调试也更简单。
    • 结构化并发出来后,对于线程(包括虚拟线程和平台线程)的管理和编排会更加的便捷方便,但目前还处在孵化中。
  2. 虚拟线程真的很廉价,针对的是IO密集操作,提升很明显。例如rpc查询、db查询等等。

  3. JDK21刚出不是很久,很多第三方库还没支持到JDK21,第三方框架没支持JDK21的情况下会有bug。