JDK 19 Virtual Threads 虚拟线程
前言
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。
线程并不是一个原子结构,包括 scheduler
和 continuation
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
Thread
和 Thread.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}
};
扩展阅读:
参考链接
我的公众号
coding 笔记、读书笔记、点滴记录,以后的文章也会同步到公众号(Coding Insight)中,希望大家关注_