JDK 19 Virtual Threads 虚拟线程

Posted by Yano on November 1, 2022

前言

JDK 19 支持了virtual thread(虚拟线程):JEP 425: Virtual Threads (Preview),虚拟线程是 Loom 项目中的一个重要特性。

Project Loom

Loom 是什么?

Loom 项目的目标是提升 Java 的并发性能。Java 自诞生就提供了线程,它是一种很方便的并发结构(先不谈线程间的通信问题 0_o),但是这种线程是使用操作系统内核线程实现的,并不能满足当前的开发需求,浪费云计算中的宝贵资源。Loom 项目引入 fiber 作为轻量级、高效的线程,fiber 由 JVM 管理,开发者可以使用和之前线程相同的操作,且 fiber 具有更好的性能、占用的内存也更少。

为什么要引入 Loom?

在二十多年前 Java 首次发布时,Java 最重要的特性之一就是能方便地访问线程,提供同步原语。Java 线程为编写并发程序提供了相对简单的抽象。但是在现在使用 Java 编写并发程序的一个问题是:并发的规模。我们希望并发服务的并发量越大越好,一个服务器能处理上万个套接字。但是由于之前的 Java 线程是使用操作系统线程实现的,在单台服务器上创建几千个套接字都很勉强了……

开发者就必须做出选择:要么直接将一个并发单元建模成线程,要么在比线程更细力度的级别上实现并发,但是需要自己编写异步代码。

Java 生态引入了异步 API,包括 JDK 的异步 NIO、异步 servlet 和异步第三方库。这些新的 API 在使用中并不优雅,而且也不容易理解,出现的原因主要是 Java 的并发单元(线程)实现得不够。仅仅是因为 Java 线程的运行时性能不够,就需要放弃线程,使用各种第三方的实现。

Java 线程使用内核线程实现固然有一些优点,比如所有的 native code 都是由内核线程支持的,所以线程中的 Java 代码能够调用 native API。但是上面提到的缺点太大了,导致难以编写高性能的代码。Erlang 和 Go 等语言都提供了轻量级线程,轻量级线程越来越流行。

Loom 项目的主要目标是添加一个通过 Java 运行时管理的叫 fiber 的轻量级线程结构,fiber 可以跟现有的中建立、操作系统的线程实现一起使用。fiber 的内存占用非常小,比内核线程轻得多,fiber 之间的任务切换开销趋近于 0。在单个 JVM 实例上就可以生成数百万个 fiber,开发者可以直接写同步阻塞的调用。同时开发者并不需要为了性能/简单性的权衡同时提供同步和异步 API。

线程并不是一个原子结构,包括 schedulercontinuation 2 个模块。Java fiber 构建在这 2 个模块之上。

Virtual threads

虚拟线程(virtual thread)就是轻量级线程,可以减少编写高吞吐高并发的应用程序的工作量。

Platform thread 是什么?

Platform thread 是操作系统线程的包装,platform Thread 在底层的 OS 线程上运行 Java 代码,数量受限于操作系统线程的数量。Platform thread 通常有比较大的线程堆栈和操作系统维护的其他资源,platform thread 也支持线程的本地变量。

可以使用 platform thread 运行所有类型的任务,就是有点浪费资源,可能会资源耗尽。

Virtual thread 是什么?

和 platform thread 一样,virtual thread 也是 java.lang.Thread 的实例。但是 virtual thread 并不与特定的操作系统线程绑定。Virtual thread 的代码仍然在操作系统的线程上运行,但是当 virtual thread 上运行的代码调用阻塞 I/O 时,Java 运行时将挂起这个 virtual thread 直到其恢复。与挂起的 virtual thread 相关联的操作系统线程可以对其他 virtual thread 执行操作。

Virtual thread 和实现方式和虚拟内存的实现方式类似。为了模拟大量内存,操作系统将一个大的虚拟地址空间映射到有限的 RAM。类似地,为了模拟大量线程,Java 运行时将大量 virtual thread 映射到少量操作系统线程上。

与 platform thread 不同,virtual thread 的调用堆栈较浅,例如只是一个 HTTP 调用或 JDBC 查询。尽管 virtual thread 支持线程局部变量,但是使用时要慎重,因为单个 JVM 可能支持数百万个 virtual thread。

Virtual thread 适合运行大部分时间被阻塞的任务(等待 I/O 操作),但并不适合 CPU 密集型操作。

Virtual thread 的好处?

Virtual thread 适合使用在高吞吐量的并发应用程序中,尤其是并发任务需要大量等待的。例如在服务器应用程序中,就需要处理很多阻塞 I/O 操作的请求。在 virtual thread 上运行的代码并不会比 platform thread 上运行的代码更快,virtual thread 的好处在于更高的吞吐量,而不是速度。

使用 virtual thread

ThreadThread.Builder API 都提供了创建 platform thread 和 virtual thread 的方法。java.util.concurrent.Executors也提供了创建使用 virtual thread 的任务的 ExecutorService

下面的代码需要使用 JDK 19,可以直接在 IDEA 中下载:

使用 Thread.Builder 创建 virtual thread

Thread thread = Thread.ofVirtual().start(() -> System.out.println("Hello"));
thread.join();

下面代码是使用 Thread.Builder 创建 2 个 virtual thread:

try {
    Thread.Builder builder =
        Thread.ofVirtual().name("worker-", 0);

    Runnable task = () -> {
        System.out.println("Thread ID: " +
            Thread.currentThread().threadId());
    };            

    // name "worker-0"
    Thread t1 = builder.start(task);   
    t1.join();
    System.out.println(t1.getName() + " terminated");

    // name "worker-1"
    Thread t2 = builder.start(task);   
    t2.join();  
    System.out.println(t2.getName() + " terminated");
    
} catch (InterruptedException e) {
    e.printStackTrace();
}

输出大概是下面这个样子:

Thread ID: 21
worker-0 terminated
Thread ID: 24
worker-1 terminated

使用 Executors.newVirtualThreadPerTaskExecutor() 创建 virtual thread

try (ExecutorService myExecutor =
    Executors.newVirtualThreadPerTaskExecutor()) {
    Future<?> future =
        myExecutor.submit(() -> System.out.println("Running thread"));
    future.get();
    System.out.println("Task completed");
} catch (InterruptedException | ExecutionException e) {
    e.printStackTrace();
}   

调度 virtual thread

Java 运行时将 virtual thread 挂载到一个 platform thread 上,操作系统像往常一样调度 platform thread。Virtual thread 可以从对应的 platform thread 上卸载(通常发生在 virtual thread 执行阻塞 I/O 操作时)。当一个 virtual thread 被卸载后,Java 运行时调度器能挂载不同的 virtual thread。

Virtual thread 也能固定到 platform thread 上,此时在阻塞操作期间也无法卸载 virtual thread。固定的情况有:

  • virtual thread 在同步块或同步方法中(synchronized)
  • virtual thread 在运行本地方法或 外部函数

补充说明

JDK 19 使用说明

要想运行上面的代码,需要几个条件:

  • IDEA 需要升级到最新版(2022.2.3),因为最新版才包含 JDK 19 的语言特性
  • 在 Project Structure 中将 Language Level 设置为 19 (Preview)

JVM 源码分析

通过查看 JDK 8 和 JDK 19 的 Thread 源码,可以发现里面的实现已经大不一样了。对于 Thread 这块又涉及大量的操作系统底层接口,最好能直接看 JDK 源码。其实有一个完全不用下载,不用配置环境查看底层源码的方法,那就是使用 Github~~~

JDK 源码在这里:https://github.com/openjdk/jdk,里面有每个 JDK 版本的源码。

我们可以直接在里面搜索文件,响应速度还可以。

比如想查看 Thread.c 源码,直接在网页上就能查看(除了跳转功能):

static JNINativeMethod methods[] = {
    {"start0",           "()V",        (void *)&JVM_StartThread},
    {"isAlive0",         "()Z",        (void *)&JVM_IsThreadAlive},
    {"setPriority0",     "(I)V",       (void *)&JVM_SetThreadPriority},
    {"yield0",           "()V",        (void *)&JVM_Yield},
    {"sleep0",           "(J)V",       (void *)&JVM_Sleep},
    {"currentCarrierThread", "()" THD, (void *)&JVM_CurrentCarrierThread},
    {"currentThread",    "()" THD,     (void *)&JVM_CurrentThread},
    {"setCurrentThread", "(" THD ")V", (void *)&JVM_SetCurrentThread},
    {"interrupt0",       "()V",        (void *)&JVM_Interrupt},
    {"holdsLock",        "(" OBJ ")Z", (void *)&JVM_HoldsLock},
    {"getThreads",       "()[" THD,    (void *)&JVM_GetAllThreads},
    {"dumpThreads",      "([" THD ")[[" STE, (void *)&JVM_DumpThreads},
    {"getStackTrace0",   "()" OBJ,     (void *)&JVM_GetStackTrace},
    {"setNativeName",    "(" STR ")V", (void *)&JVM_SetNativeThreadName},
    {"extentLocalCache",  "()[" OBJ,    (void *)&JVM_ExtentLocalCache},
    {"setExtentLocalCache", "([" OBJ ")V",(void *)&JVM_SetExtentLocalCache},
    {"getNextThreadIdOffset", "()J",     (void *)&JVM_GetNextThreadIdOffset}
};

扩展阅读:

参考链接

GitHub 项目

Java 编程思想-最全思维导图-GitHub 下载链接,需要的小伙伴可以自取~

原创不易,希望大家转载时请先联系我,并标注原文链接。

我的公众号

coding 笔记、读书笔记、点滴记录,以后的文章也会同步到公众号(Coding Insight)中,希望大家关注^_^