Java 21 最重要的功能之一是虚拟线程 ( JEP 444 )。这些轻量级线程减少了编写、维护和观察高吞吐量并发应用程序所需的工作量。
正如我的许多其他文章一样,在推出新功能之前,让我们先看看现状,以便更好地了解它试图解决的问题以及好处是什么。在引入虚拟线程之前,我们习惯的线程java.lang.Thread是支持所谓的_平台线程_的。
这些线程通常以 1:1 的方式映射到操作系统调度的内核线程。操作系统线程相当“重”。这使得它们适合执行所有类型的任务。
根据操作系统和配置的不同,它们默认消耗 2 到 10 MB 之间的空间。因此,如果您想在重负载并发应用程序中使用一百万个线程,您最好有超过 2 TB 的空闲内存!
正如您所看到的,有一个明显的瓶颈限制了我们实际上可以拥有的线程数量,而没有任何缺点。
每个请求一个线程
这是有问题的,因为它与“每个请求一个线程”的典型服务器应用程序方法直接冲突。每个请求使用单个线程有很多优点,例如更容易的状态管理和清理。但它也造成了可扩展性限制。应用程序的“并发单元”(在本例中为请求)需要单个“并发平台单元”。因此,线程更容易作为原始 CPU 功率或网络而耗尽。
尽管“每个请求一个线程”有很多优点,共享重量级线程可以更均匀地利用您的硬件,但需要一种完全不同的方法。
异步救援
它的每个部分不是在单个线程上运行整个请求,而是在任务完成时使用池中的线程,因此另一个任务可能会重用同一线程。这允许您的代码需要更少的线程,但会带来_异步_编程的负担。
异步编程具有自己的范例,具有一定的学习曲线,并且可能使您的程序更难以理解和遵循。请求的每个部分可能在不同的线程上执行,在没有合理上下文的情况下创建堆栈跟踪,并使调试变得非常棘手甚至几乎不可能。
Java 有一个优秀的异步编程 API,CompletableFuture。我什至在我的书中使用该 API 编写了大约 25 页关于异步任务的内容,但我仍然认为这是一个复杂的 API,并且不太适合许多 Java 开发人员习惯的思维方式。
重新审视“每个请求一个线程”模型,很明显,一种更轻量级的线程方法可以解决瓶颈,并提供一种熟悉的做事方式。
轻量级线程
由于平台线程的数量在没有更多硬件的情况下无法改变,因此需要另一层抽象,以切断首先产生瓶颈的可怕的 1:1 映射。
轻量级线程不依赖于特定的平台线程,并且不带有为其分配的大量内存。它们由运行时而不是底层操作系统调度和管理。这就是为什么可以创建大量轻量级线程的原因。
一般概念并不新鲜,许多语言都有某种形式的轻量级线程:
Java 最终在版本 21 中引入了自己的轻量级线程实现:虚拟线程。
在我看来,虚拟线程最好的事情之一是,您不需要学习新的范例或复杂的新 API,就像异步编程所需要的那样。相反,您可以像对待非虚拟线程一样对待它们。
创建平台线程
创建平台线程很简单,就像使用创建一样Runnable:
Runnable fn = () -> {
};
Thread 线程 = new Thread
(fn).start();
随着Project Loom简化了新的并发方法,它还提供了一种创建平台支持的线程的新方法:
线程 线程 = Thread.ofPlatform()。
.start(可运行);
实际上,现在有一个完整的 Fluent API,它ofPlatform()返回一个[Thread.Builder.OfPlatform](https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/Thread.Builder.OfPlatform.html)实例:
线程 线程 = Thread.ofPlatform()。
.daemon()
.name( "my-custom-thread" )
.unstarted(runnable);
但您来这里不是为了学习创建“旧”线程的新方法,您想要的是新东西!
创建虚拟线程
对于虚拟线程,同样有一个流畅的 API:
Runnable fn = () -> {
};
Thread 线程 =
Thread.ofVirtual(fn)
.start();
除了构建器方法之外,您可以Runnable直接执行:
Thread thread = Thread.startVirtualThread(() -> {
});
join()由于所有虚拟线程始终都是守护线程,因此如果您想在主线程上等待,请不要忘记调用。
创建虚拟线程的另一种方法是使用 Executor:
var executorService = Executors.newVirtualThreadPerTaskExecutor(); executorService.submit(() -> {
});