Java 21 版本更新中最重要的功能之一就是虚拟线程 (JEP 444)。这些轻量级线程减少了编写、维护和观察高吞吐量并发应用程序所需的工作量。
正如我的许多其他文章一样,在推出新功能之前,让我们先看看 Java 21 版本更新前的现状,以便更好地了解 Java 21 版本试图解决的问题以及好处是什么。
平台线程
在引入虚拟线程之前,java.lang.Thread 包已经支持所谓的平台线程。
这些线程通常以 1:1 的方式映射到操作系统调度的内核线程。操作系统线程相当“重”。这使得它们可以执行所有类型的任务。
根据操作系统和 JVM 启动参数配置的不同,一个平台线程默认消耗 1 MB 的空间。因此如果我们想在重负载高并发应用程序中使用一百万个线程,我们最好有超过 1 TB 的空闲内存!
如上所述,平台线程有一个明显的内存瓶颈限制了我们实际上可以拥有的线程数量。
每个请求一个线程
每个请求使用单个线程有很多优点,例如更容易的状态管理和清理。但它也造成了可扩展性限制。应用程序的“并发单元”(在本例中为请求)需要单个“并发平台单元”(在本例中也就是平台线程),但是在重负载高并发应用程序中,平台线程容易因为内存不足、CPU 资源耗尽而创建失败。
尽管“每个请求一个线程”有很多优点,平台线程可以更均匀地利用硬件,但我们还是需要一种完全不同的方法。
使用线程池
与在单个线程上处理以个请求不同,当任务完成时,线程会被线程池回收,因此另一个请求可能会重用相同的线程。这允许我们的程序使用更少的线程处理更多的请求,但会带来异步编程的负担。
异步编程具有自己的范例,具有一定的学习曲线,并且可能使我们的程序更难以理解和遵循。请求的每个部分可能在不同的线程上执行,在没有合理上下文的情况下创建堆栈跟踪,并使调试变得非常棘手甚至几乎不可能。
重新审视“每个请求一个线程”模型,很明显,我们需要一种更轻量级的线程方法来解决这个瓶颈,并最好按照我们熟悉的方式。
轻量级线程
由于平台线程的数量在不新增硬件资源的情况下无法改变,因此也就需要另一层抽象,以切断首先产生瓶颈的可怕的 1:1 映射。
轻量级线程不依赖于特定的平台线程,也不会为其分配大量内存。它们由运行时的 JVM 调度和管理而不是底层操作系统。这就是为什么可以创建大量轻量级线程的原因。
轻量级线程的概念并不新鲜,许多语言都有某种形式的轻量级线程:
- Go 语言中的 Goroutines(协程)
- Erlang 语言中的 Processes(轻量级进程)
- Haskell Threads
Java 也在 21 版本中引入了自己的轻量级线程实现:虚拟线程。
虚拟线程
虚拟线程是一个新的轻量级 java.lang.Thread 变体,是 Project Loom 项目的一部分,不由操作系统管理或调度。相反由 JVM 负责调度。当然在实际工作反映到操作系统还是以平台线程运行,但 JVM 正是利用所谓的载体线程(即平台线程)来“承载”虚拟线程,以便在需要时执行。
JVM / OS 线程调度示意图
所需的平台线程以 FIFO 工作方式在 ForkJoinPool 中进行管理,默认情况下,它使用所有可用的处理器,但可以通过调整系统属性 jdk.virtualThreadScheduler.parallelism 来根据我们的要求进行修改。我们熟悉的 ForkJoinPool 与并行流等其他功能使用的公共池之间的主要区别在于,公共池以 LIFO 模式运行。
物美价廉
虚拟线程是廉价且轻量级的,我们可以使用“每个请求一个线程”模型,而不必担心实际需要多少个线程。如果我们的代码在虚拟线程中调用阻塞 I/O 操作,则运行时会挂起这个被阻塞的虚拟线程,直到挂起结束后就可以恢复。这样一来,程序对硬件的利用就可以达到近乎最佳并提供高水平的并发性,从而实现高吞吐量。
因为虚拟线程非常便宜,所以虚拟线程不会被重用或需要被池化。每个任务都由其自己的虚拟线程来执行。
设定界限
JVM 调度程序通过载体线程来管理虚拟线程,因此需要一定的边界和分隔来确保可能的“无数”虚拟线程按预期运行。这是通过在载体线程和它可能承载的任何虚拟线程之间保持无线程关联来实现的:
- 虚拟线程无法访问载体线程,Thread.currentThread() 返回虚拟线程本身。
- 堆栈跟踪是独立的,虚拟线程中抛出的任何异常仅包含其自己的堆栈帧。
- 虚拟线程的线程局部变量对其载体线程不可用,反之亦然。
- 从代码的角度来看,载体线程及其虚拟线程对平台线程的共享是不可见的。
代码展示
在我看来,虚拟线程最好的事情之一就是我们不需要学习新的编程范例或复杂的新 API,就能够完成异步编程。在使用上,我们可以像对待平台线程一样对待虚拟线程。
创建平台线程
创建平台线程很简单,就像使用 Runnable 创建一样:
Runnable fn = () -> {
// your code here
};
Thread thread = new Thread(fn).start();
随着 Project Loom 项目简化了新的并发方法,还提供了一种创建平台线程的新方法:
Thread thread = Thread.ofPlatform().
.start(runnable);
实际上,现在有一个完整的 Fluent API,因为 ofPlatform() 返回一个 Thread.Builder.OfPlatform 实例:
Thread thread = Thread.ofPlatform().
.daemon()
.name("my-custom-thread")
.unstarted(runnable);
但你来这里显然不是为了学习创建“旧”线程的新方法,你想要新的东西!
创建虚拟线程
对于虚拟线程,同样有一个 Fluent API:
Runnable fn = () -> {
// your code here
};
Thread thread = Thread.ofVirtual(fn)
.start();
除了构建器方法之外,我们还可以直接使用以下命令创建虚拟线程:
Thread thread = Thread.startVirtualThread(() -> {
// your code here
});
由于所有虚拟线程始终都是守护线程,因此如果我们想在主线程上等待虚拟线程执行完毕,可以调用 join() 方法。
创建虚拟线程的另一种方法是使用 Executor 类:
var executorService = Executors.newVirtualThreadPerTaskExecutor();
executorService.submit(() -> {
// your code here
});
总结
尽管作用域值 (JEP 446) 和结构化并发 (JEP 453) 仍然是 Java 21 中的预览功能,但虚拟线程已经成为可投入生产的成熟功能。
虚拟线程是一种通用且强大的 Java 并发新方式,将对我们的未来程序产生重大影响。虚拟线程使用熟悉且可靠的“每个请求一个线程”方法,同时以最佳方式利用所有可用硬件,无需学习新范例或复杂的 API。
本文翻译自国外论坛 medium,原文地址:https://medium.com/@benweidig/looking-at-java-21-virtual-threads-0ddda4ac1be1