Virtual Thread support reference
本指南解释了如何在 Quarkus 应用程序中受益于 Java 21+ 的虚拟线程。
This guide explains how to benefit from Java 21+ virtual threads in Quarkus application.
What are virtual threads?
Terminology
- OS thread
-
A "thread-like" data structure managed by the Operating System.
- Platform thread
-
Until Java 19, every instance of the Thread class was a platform thread, a wrapper around an OS thread. Creating a platform thread creates an OS thread, and blocking a platform thread blocks an OS thread.
- Virtual thread
-
Lightweight, JVM-managed threads. They extend the Thread class but are not tied to one specific OS thread. Thus, scheduling virtual threads is the responsibility of the JVM.
- Carrier thread
-
A platform thread used to execute a virtual thread is called a carrier thread. It isn’t a class distinct from Thread or
VirtualThread
but rather a functional denomination.
Differences between virtual threads and platform threads
我们将在此处简要概述该主题;有关更多信息,请参阅 JEP 425。
We will give a brief overview of the topic here; please refer to the JEP 425 for more information.
虚拟线程是自 Java 19 起可用的功能(Java 21 是第一个包括虚拟线程的 LTS 版本),旨在为面向 I/O 的工作负载提供一个廉价的平台线程替代品。
Virtual threads are a feature available since Java 19 (Java 21 is the first LTS version including virtual threads), aiming at providing a cheap alternative to platform threads for I/O-bound workloads.
到目前为止,平台线程是 JVM 的并发单元。它们是对操作系统结构的包装。创建 Java 平台线程将在您的操作系统中创建一个“类线程”结构。
Until now, platform threads were the concurrency unit of the JVM. They are a wrapper over OS structures. Creating a Java platform thread creates a "thread-like" structure in your operating system.
另一方面,虚拟线程由 JVM 管理。要执行它们,需要将其装载到平台线程上(充当该虚拟线程的载体)。因此,它们被设计为提供以下特性:
Virtual threads, on the other hand, are managed by the JVM. To be executed, they need to be mounted on a platform thread (which acts as a carrier to that virtual thread). As such, they have been designed to offer the following characteristics:
- Lightweight
-
Virtual threads occupy less space than platform threads in memory. Hence, it becomes possible to use more virtual threads than platform threads simultaneously without blowing up the memory. By default, platform threads are created with a stack of about 1 MB, whereas virtual threads stack is "pay-as-you-go." You can find these numbers and other motivations for virtual threads in this presentation given by the lead developer of project Loom (the project that added the virtual thread support to the JVM).
- Cheap to create
-
Creating a platform thread in Java takes time. Currently, techniques such as pooling, where threads are created once and then reused, are strongly encouraged to minimize the time lost in starting them (as well as limiting the maximum number of threads to keep memory consumption low). Virtual threads are supposed to be disposable entities that we create when we need them, it is discouraged to pool them or reuse them for different tasks.
- Cheap to block
-
When performing blocking I/O, the underlying OS thread wrapped by the Java platform thread is put in a wait queue, and a context switch occurs to load a new thread context onto the CPU core. This operation takes time. Since the JVM manages virtual threads, no underlying OS thread is blocked when they perform a blocking operation. Their state is stored in the heap, and another virtual thread is executed on the same Java platform (carrier) thread.
The Continuation Dance
如上所述,JVM 调度虚拟线程。这些虚拟线程装载在载体线程上。调度带有一点魔法。当虚拟线程试图使用阻塞 I/O 时,JVM _transforms_将此调用转换为非阻塞调用,卸载虚拟线程并将另一个虚拟线程装载到载体线程上。当 I/O 完成后,_waiting_虚拟线程再次符合条件,并将重新装载到载体线程上继续执行。对于用户来说,所有这一切都是无形的。您的同步代码将异步执行。
As mentioned above, the JVM schedules the virtual threads. These virtual threads are mounted on carrier threads. The scheduling comes with a pinch of magic. When the virtual thread attempts to use blocking I/O, the JVM transforms this call into a non-blocking one, unmounts the virtual thread, and mounts another virtual thread on the carrier thread. When the I/O completes, the waiting virtual thread becomes eligible again and will be re-mounted on a carrier thread to continue its execution. For the user, all this dance is invisible. Your synchronous code is executed asynchronously.
请注意,虚拟线程可能不会重新装载到相同的载体线程上。
Note that the virtual thread may not be re-mounted on the same carrier thread.
Virtual threads are useful for I/O-bound workloads only
我们现在知道,我们可以创建比平台线程更多的虚拟线程。人们可能会倾向于使用虚拟线程执行长时间计算(CPU 绑定工作负载)。这是无用且适得其反的。CPU 绑定不包含快速交换线程,而是在等待 I/O 完成时将它们附加到 CPU 核心以计算某些内容。在这种情况下,如果我们有十几个 CPU 核心,拥有数千个线程比无用更糟,虚拟线程不会提高 CPU 绑定工作负载的性能。更糟糕的是,当在虚拟线程上运行 CPU 绑定工作负载时,虚拟线程会独占其装载的载体线程。它要么会减少其他虚拟线程运行的机会,要么会开始创建新的载体线程,从而导致内存使用量高。
We now know we can create more virtual threads than platform threads. One could be tempted to use virtual threads to perform long computations (CPU-bound workload). It is useless and counterproductive. CPU-bound doesn’t consist of quickly swapping threads while they need to wait for the completion of an I/O, but in leaving them attached to a CPU core to compute something. In this scenario, it is worse than useless to have thousands of threads if we have tens of CPU cores, virtual threads won’t enhance the performance of CPU-bound workloads. Even worse, when running a CPU-bound workload on a virtual thread, the virtual thread monopolizes the carrier thread on which it is mounted. It will either reduce the chance for the other virtual thread to run or will start creating new carrier threads, leading to high memory usage.
Run code on virtual threads using @RunOnVirtualThread
在 Quarkus 中,使用 @RunOnVirtualThread注释实现了对虚拟线程的支持。本节简要概述了基本原理以及如何使用它。有专门的指南支持该注释的扩展,例如:
In Quarkus, the support of virtual thread is implemented using the @RunOnVirtualThread annotation. This section briefly overviews the rationale and how to use it. There are dedicated guides for extensions supporting that annotation, such as:
Why not run everything on virtual threads?
如上所述,并非所有内容都可以在虚拟线程上安全运行。monopolization*风险可能导致内存使用量高。此外,还有虚拟线程无法从载体线程卸载的情况。这称为 *pinning。最后,一些库使用 `ThreadLocal`存储和重用对象。对这些库使用虚拟线程会导致大量分配,因为有意池化的对象将被实例化用于每个(可一次性使用并且通常寿命短的)虚拟线程。
As mentioned above, not everything can run safely on virtual threads.
The risk of monopolization can lead to high-memory usage.
Also, there are situations where the virtual thread cannot be unmounted from the carrier thread.
This is called pinning.
Finally, some libraries use ThreadLocal
to store and reuse objects.
Using virtual threads with these libraries will lead to massive allocation, as the intentionally pooled objects will be instantiated for every (disposable and generally short-lived) virtual thread.
截至今天,不可能在不担忧的方式使用虚拟线程。遵循这种放任自流的方法可能会很快导致内存和资源匮乏问题。因此,Quarkus 使用一个明确的模型,直到上述问题消失(随着 Java 生态系统的成熟)。这也是 _reactive_扩展具有虚拟线程支持,而 _classic_扩展很少具有虚拟线程支持的原因。我们需要知道何时在虚拟线程上分派。
As of today, it is not possible to use virtual threads in a carefree manner. Following such a laissez-faire approach could quickly lead to memory and resource starvation issues. Thus, Quarkus uses an explicit model until the aforementioned issues disappear (as the Java ecosystem matures). It is also the reason why reactive extensions have the virtual thread support, and rarely the classic ones. We need to know when to dispatch on a virtual thread.
重要的是要理解,这些问题不是 Quarkus 的限制或错误,而是由于 Java 生态系统的当前状态需要演变才能变得对虚拟线程友好。
It is essential to understand that these issues are not Quarkus limitations or bugs but are due to the current state of the Java ecosystem which needs to evolve to become virtual thread friendly.
To learn more about the internal design and choices, check the Considerations for integrating virtual threads in a Java framework: a Quarkus example in a resource-constrained environment paper. |
Monopolization cases
独占性已在 Virtual threads are useful for I/O-bound workloads only部分中进行了说明。在运行长时间计算时,我们不允许 JVM 卸载并切换到另一个虚拟线程,直到虚拟线程终止。事实上,当前的调度程序不支持抢占任务。
The monopolization has been explained in the cpu-bound section. When running long computations, we do not allow the JVM to unmount and switch to another virtual thread until the virtual thread terminates. Indeed, the current scheduler does not support preempting tasks.
这种独占可能会导致创建新的载体线程以执行其他虚拟线程。创建载体线程会导致创建平台线程。因此,与此创建关联有一个内存成本。
This monopolization can lead to the creation of new carrier threads to execute other virtual threads. Creating carrier threads results in creating platform threads. So, there is a memory cost associated with this creation.
假设您在受限环境(例如容器)中运行。在这种情况下,独占很快会成为一个问题,因为高内存使用可能会导致内存不足问题和容器终止。由于调度和虚拟线程的固有成本,内存使用量可能高于常规工作线程。
Suppose you run in a constrained environment, such as containers. In that case, monopolization can quickly become a concern, as the high memory usage can lead to out-of-memory issues and container termination. The memory usage may be higher than with regular worker threads because of the inherent cost of the scheduling and virtual threads.
Pinning cases
“低成本阻塞”的承诺不一定总能成立:虚拟线程有时可能会_pin_其载体。在这种情况下,平台线程会被阻塞,这与典型的阻塞场景中完全一样。
The promise of "cheap blocking" might not always hold: a virtual thread might pin its carrier on certain occasions. The platform thread is blocked in this situation, precisely as it would have been in a typical blocking scenario.
根据 JEP 425,这种情况可能会在以下两种场景中发生:
According to JEP 425 this can happen in two situations:
-
when a virtual thread performs a blocking operation inside a
synchronized
block or method -
when it executes a blocking operation inside a native method or a foreign function
在代码中避开这些情况可能比较容易,但验证每种您使用的依赖项都很困难。通常,在尝试使用虚拟线程的时候,我们发现 postgresql-JDBC driver的 42.6.0 之前的版本会导致频繁的固定。大部分 JDBC 驱动程序仍然固定载体线程。更糟糕的是,许多库都需要进行代码更改。
It can be reasonably easy to avoid these situations in your code, but verifying every dependency you use is hard. Typically, while experimenting with virtual threads, we realized that versions older than 42.6.0 of the postgresql-JDBC driver result in frequent pinning. Most JDBC drivers still pin the carrier thread. Even worse, many libraries require code changes.
有关详细信息,请参见 When Quarkus meets Virtual Threads
For more information, see When Quarkus meets Virtual Threads
有关固定案例的信息适用于 PostgreSQL JDBC 驱动程序 42.5.4 及更早版本。对于 PostgreSQL JDBC 驱动程序 42.6.0 及更高版本,几乎所有同步方法都已被可重入锁取代。有关详细信息,请参阅 PostgreSQL JDBC 驱动程序 42.6.0 的 Notable Changes
This information about pinning cases applies to PostgreSQL JDBC driver 42.5.4 and earlier. For PostgreSQL JDBC driver 42.6.0 and later, virtually all synchronized methods have been replaced by reentrant locks. For more information, see the Notable Changes for PostgreSQL JDBC driver 42.6.0.
The pooling case
某些库使用`ThreadLocal`作为对象池机制。像 Jackson和 Netty 这样的非常流行的库假定应用程序使用有限数量的线程,并且这些线程被回收利用(使用线程池)来运行多个(不相关但顺序执行)的任务。
Some libraries are using ThreadLocal
as an object pooling mechanism.
Extremely popular libraries like Jackson and Netty assume that the application uses a limited number of threads, which are recycled (using a thread pool) to run multiple (unrelated but sequential) tasks.
这种模式有多种优势,例如:
This pattern has multiple advantages, such as:
-
Allocation benefit: heavy objects are only allocated once per thread, but because the number of these threads was intended to be limited, it would not use too much memory.
-
Thread safety: only one thread can access the object stored in the thread local - preventing concurrent accesses.
但是,在使用虚拟线程时,这种模式会适得其反。虚拟线程没有池,而且通常是短命的。因此,我们现在有很多虚拟线程,而不是少数的几个虚拟线程。对于其中每一个,都创建存储在`ThreadLocal`中的对象(通常又大又昂贵),并且不会重用,因为虚拟线程没有池(而且不会在执行完成后用于运行另一个任务)。这种问题会导致大量使用内存。很不幸,这需要在库自身进行复杂的代码更改。
However, this pattern is counter-productive when using virtual threads.
Virtual threads are not pooled and generally short-lived.
So, instead of a few of them, we now have many of them.
For each of them, the object stored in the ThreadLocal
is created (often large and expensive) and won’t be reused, as the virtual thread is not pooled (and won’t be used to run another task once the execution completes).
This problem leads to high memory usage.
Unfortunately, it requires sophisticated code changes in the libraries themselves.
Use @RunOnVirtualThread with Quarkus REST (formerly RESTEasy Reactive)
本部分展示了使用 @RunOnVirtualThread注释的一个简短示例。它还解释了 Quarkus 提供的各种开发和执行模型。
This section shows a brief example of using the @RunOnVirtualThread annotation. It also explains the various development and execution models offered by Quarkus.
`@RunOnVirtualThread`注释指示 Quarkus 调用*new*虚拟线程中的注释方法而不是当前方法。Quarkus 处理虚拟线程的创建和卸载。
The @RunOnVirtualThread
annotation instructs Quarkus to invoke the annotated method on a new virtual thread instead of the current one.
Quarkus handles the creation of the virtual thread and the offloading.
由于虚拟线程是可处置实体,因此`@RunOnVirtualThread`的基本理念是在新虚拟线程中卸载端点处理器的执行,而不是在事件循环或工作线程(对于 Quarkus REST 是这样)中运行它。
Since virtual threads are disposable entities, the fundamental idea of @RunOnVirtualThread
is to offload the execution of an endpoint handler on a new virtual thread instead of running it on an event-loop or worker thread (in the case of Quarkus REST).
要实现这一点,可以向端点添加 @RunOnVirtualThread注释就足够了。如果用于*run*应用程序的 Java 虚拟机提供虚拟线程支持(Java 21 或更高版本),则端点执行将卸载到虚拟线程。然后将有能力在不阻塞安装虚拟线程的平台线程情况下执行阻塞操作。
To do so, it suffices to add the @RunOnVirtualThread annotation to the endpoint. If the Java Virtual Machine used to run the application provides virtual thread support (so Java 21 or later versions), then the endpoint execution is offloaded to a virtual thread. It will then be possible to perform blocking operations without blocking the platform thread upon which the virtual thread is mounted.
对于 Quarkus REST,此注释只能用于使用 @Blocking注释的端点或由于其签名而被视为阻塞的端点。可以访问Execution model, blocking, non-blocking以了解详细信息。
In the case of Quarkus REST, this annotation can only be used on endpoints annotated with @Blocking or considered blocking because of their signature. You can visit Execution model, blocking, non-blocking for more information.
Get started with virtual threads with Quarkus REST
将以下依赖项添加到构建文件中:
Add the following dependency to your build file:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest</artifactId>
</dependency>
implementation("io.quarkus:quarkus-rest")
然后,还需要确保使用 Java 21 及更高版本,这可以在 pom.xml 文件中使用以下内容强制执行:
Then, you also need to make sure that you are using Java 21+, this can be enforced in your pom.xml file with the following:
<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
</properties>
Three development and execution models
下面的示例显示了三个端点之间的区别,所有这些都在数据库中查询 fortune ,然后将其返回给客户端。
The example below shows the differences between three endpoints, all of them querying a fortune in the database then returning it to the client.
-
the first one uses the traditional blocking style, it is considered blocking due to its signature.
-
the second one uses Mutiny, it is considered non-blocking due to its signature.
-
the third one uses Mutiny but in a synchronous way, since it doesn’t return a "reactive type" it is considered blocking and the @RunOnVirtualThread annotation can be used.
package org.acme.rest;
import org.acme.fortune.model.Fortune;
import org.acme.fortune.repository.FortuneRepository;
import io.smallrye.common.annotation.RunOnVirtualThread;
import io.smallrye.mutiny.Uni;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import java.util.List;
import java.util.Random;
@Path("")
public class FortuneResource {
@Inject FortuneRepository repository;
@GET
@Path("/blocking")
public Fortune blocking() {
// Runs on a worker (platform) thread
var list = repository.findAllBlocking();
return pickOne(list);
}
@GET
@Path("/reactive")
public Uni<Fortune> reactive() {
// Runs on the event loop
return repository.findAllAsync()
.map(this::pickOne);
}
@GET
@Path("/virtual")
@RunOnVirtualThread
public Fortune virtualThread() {
// Runs on a virtual thread
var list = repository.findAllAsyncAndAwait();
return pickOne(list);
}
}
下表总结了这些选项:
The following table summarizes the options:
Model | Example of signature | Pros | Cons |
---|---|---|---|
Synchronous code on worker thread |
|
Simple code |
Use worker thread (limit concurrency) |
Reactive code on event loop |
|
High concurrency and low resource usage |
More complex code |
Synchronous code on virtual thread |
|
Simple code |
Risk of pinning, monopolization and under-efficient object pooling |
请注意,所有这三个模型都可以在单个应用程序中使用。
Note that all three models can be used in a single application.
Use virtual thread friendly clients
如 Why not run everything on virtual threads? 部分中所述,Java 生态系统还没有完全准备好使用虚拟线程。所以,您需要小心,尤其是在使用执行 I/O 的库时。
As mentioned in the why-not section, the Java ecosystem is not entirely ready for virtual threads. So, you need to be careful, especially when using a libraries doing I/O.
幸运的是,Quarkus 提供了一个巨大的生态系统,可以用于虚拟线程。Quarkus 中使用的反应型编程库 Mutiny 和 Vert.x Mutiny 绑定提供了编写阻塞代码(所以,不用担心,没有学习曲线)的能力,而不会钉住载波线程。
Fortunately, Quarkus provides a massive ecosystem that is ready to be used in virtual threads. Mutiny, the reactive programming library used in Quarkus, and the Vert.x Mutiny bindings provides the ability to write blocking code (so, no fear, no learning curve) which do not pin the carrier thread.
所以:
As a result:
-
Quarkus extensions providing blocking APIs on top of reactive APIs can be used in virtual threads. This includes the REST Client, the Redis client, the mailer…
-
API returning
Uni
can be used directly usinguni.await().atMost(…)
. It blocks the virtual thread, without blocking the carrier thread, and also improves the resilience of your application with an easy (non-blocking) timeout support. -
If you use a Vert.x client using the Mutiny bindings, use the
andAwait()
methods which block until you get the result without pinning the carrier thread. It includes all the reactive SQL drivers.
Detect pinned thread in tests
我们建议在应用程序中使用虚拟线程运行测试时使用以下配置。如果不会使测试失败,但至少在代码钉住载波线程时转储启动跟踪:
We recommend to use the following configuration when running tests in application using virtual threads. If would not fail the tests, but at least dump start traces if the code pins the carrier thread:
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>${surefire-plugin.version}</version>
<configuration>
<systemPropertyVariables>
<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
<maven.home>${maven.home}</maven.home>
</systemPropertyVariables>
<argLine>-Djdk.tracePinnedThreads</argLine>
</configuration>
</plugin>
Run application using virtual threads
java -jar target/quarkus-app/quarkus-run.jar
在 Java 21 之前,虚拟线程仍然是一个实验性功能,你需要使用 |
Prior to Java 21, virtual threads were still an experimental feature, you need to start your application with the |
Build containers for application using virtual threads
在 JVM 模式下运行应用程序时(因此没有编译成本机模式,对于本机模式,请查看 the dedicated section),你可以按照 containerization guide 创建容器。
When running your application in JVM mode (so not compiled into native, for native check native), you can follow the containerization guide to build a container.
在本节中,我们使用 JIB 来构建容器。请参阅 containerization guide 以了解有关其他方法的更多信息。
In this section, we use JIB to build the container. Refer to the containerization guide to learn more about the alternatives.
要将使用 @RunOnVirtualThread
的 Quarkus 应用程序容器化,请在 application.properties
中添加以下属性:
To containerize your Quarkus application that use @RunOnVirtualThread
, add the following properties in your application.properties
:
quarkus.container-image.build=true
quarkus.container-image.group=<your-group-name>
quarkus.container-image.name=<you-container-name>
quarkus.jib.base-jvm-image=registry.access.redhat.com/ubi8/openjdk-21-runtime 1
quarkus.jib.platforms=linux/amd64,linux/arm64 2
1 | Make sure you use a base image supporting virtual threads. Here we use an image providing Java 21. Quarkus picks an image providing Java 21+ automatically if you do not set one. |
2 | Select the target architecture. You can select more than one to build multi-archs images. |
然后,按照通常的方法构建容器。例如,如果你使用 Maven,请运行:
Then, build your container as you would do usually. For example, if you are using Maven, run:
mvn package
Compiling Quarkus application using virtual threads into native executable
Using a local GraalVM installation
若要将利用 @RunOnVirtualThread
的 Quarkus 应用程序编译成本机可执行文件,你必须确保使用支持虚拟线程的 GraalVM/Mandrel native-image
,即至少提供 Java 21。
To compile a Quarkus applications leveraging @RunOnVirtualThread
into a native executable, you must be sure to use a GraalVM / Mandrel native-image
supporting virtual threads, so providing at least Java 21.
按照 the native compilation guide 中的说明构建本机可执行文件。例如,使用 Maven,请运行:
Build the native executable as indicated on the native compilation guide. For example, with Maven, run:
mvn package -Dnative
Using an in-container build
容器内构建允许使用在容器内运行的 native-image
编译器构建 Linux 64 可执行文件。这样可以避免在你的机器上安装 native-image
,还可以配置所需的 GraalVM 版本。请注意,要使用容器内构建,你的机器上必须安装 Docker 或 Podman。
In-container build allows building Linux 64 executables by using a native-image
compiler running in a container.
It avoids having to install native-image
on your machine, and also allows configuring the GraalVM version you need.
Note that, to use in-container build, you must have Docker or Podman installed on your machine.
然后,将以下内容添加到 application.properties
文件中:
Then, add to your application.properties
file:
# In-container build to get a linux 64 executable
quarkus.native.container-build=true 1
1 | Enables the in-container build |
如果你使用的是 Mac M1 或 M2(使用 ARM64 CPU),则需要知道你使用容器内构建获得的本机可执行文件将是 Linux 可执行文件,但使用的是主机(ARM 64)架构。使用 Docker 时,你可以使用以下属性强制设置架构:
If you are using a Mac M1 or M2 (using an ARM64 CPU), you need to be aware that the native executable you will get using an in-container build will be a Linux executable, but using your host (ARM 64) architecture. You can use emulation to force the architecture when using Docker with the following property:
quarkus.native.container-runtime-options=--platform=linux/amd64
请注意,这会大大增加编译时间(>10 分钟)。
Be aware that it increases the compilation time… a lot (>10 minutes).
Containerize native applications using virtual threads
要构建运行 Quarkus 应用程序的容器(使用编译成本机可执行文件的虚拟线程),你必须确保拥有 Linux/AMD64 可执行文件(或 ARM64,如果你针对的是 ARM 机器)。
To build a container running a Quarkus application using virtual threads compiled into a native executable, you must make sure you have a Linux/AMD64 executable (or ARM64 if you are targeting ARM machines).
确保你的 application.properties
包含 the native compilation section 中说明的配置。
Make sure your application.properties
contains the configuration explained in native.
然后,按照通常的方法构建容器。例如,如果你使用 Maven,请运行:
Then, build your container as you would do usually. For example, if you are using Maven, run:
mvn package -Dnative
如果你想要构建本机容器镜像,并且已经有现有本机镜像,则可以设置 |
If you ever want to build a native container image and already have an existing native image you can set |
Use the duplicated context in virtual threads
使用 @RunOnVirtualThread
注释的方法从原始重复上下文继承(有关详细信息,请参见 duplicated context reference guide)。因此,在方法执行期间,过滤器和拦截器写入重复上下文(以及请求范围,因为请求范围存储在重复上下文中)的数据可用(即使过滤器和拦截器没有在虚拟线程上运行)。
Methods annotated with @RunOnVirtualThread
inherit from the original duplicated context (See the duplicated context reference guide for details).
So, the data written in the duplicated context (and the request scope, as the request scoped is stored in the duplicated context) by filters and interceptors are available during the method execution (even if the filters and interceptors are not run on the virtual thread).
但是,不会传播线程局部变量。
However, thread locals are not propagated.
Virtual thread names
默认情况下,虚拟线程是在没有线程名称的情况下创建的,这对于识别调试和日志记录目的的执行并不实用。Quarkus 管理的虚拟线程被命名并以 `quarkus-virtual-thread-`为前缀。您可以自定义此前缀,或通过配置空值完全禁用该命名:
Virtual threads are created without a thread name by default, which is not practical to identify the execution for debugging and logging purposes.
Quarkus managed virtual threads are named and prefixed with quarkus-virtual-thread-
.
You can customize this prefix, or disable the naming altogether configuring an empty value:
quarkus.virtual-threads.name-prefix=
Inject the virtual thread executor
为了在虚拟线程上运行任务,Quarkus 管理了一个内部 ThreadPerTaskExecutor
。在需要直接访问该执行程序的罕见情况下,您可以使用 @VirtualThreads
CDI 限定符注入它:
In order to run tasks on virtual threads Quarkus manages an internal ThreadPerTaskExecutor
.
In rare instances where you’d need to access this executor directly you can inject it using the @VirtualThreads
CDI qualifier:
注入虚拟线程 ExecutorService 是实验性的,并且可能在未来版本中发生变化。
Injecting the Virtual Thread ExecutorService is experimental and may change in future versions.
package org.acme;
import org.acme.fortune.repository.FortuneRepository;
import java.util.concurrent.ExecutorService;
import jakarta.enterprise.event.Observes;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import io.quarkus.logging.Log;
import io.quarkus.runtime.StartupEvent;
import io.quarkus.virtual.threads.VirtualThreads;
public class MyApplication {
@Inject
FortuneRepository repository;
@Inject
@VirtualThreads
ExecutorService vThreads;
void onEvent(@Observes StartupEvent event) {
vThreads.execute(this::findAll);
}
@Transactional
void findAll() {
Log.info(repository.findAllBlocking());
}
}
Testing virtual thread applications
如上所述,虚拟线程有一些限制,这些限制会极大地影响您的应用程序性能和内存使用。 junit5-virtual-threads 扩展提供了一种在运行测试时检测已固定的载体线程的方法。因此,您可以消除最突出的限制之一或了解问题。
As mentioned above, virtual threads have a few limitations that can drastically affect your application performance and memory usage. The junit5-virtual-threads extension provides a way to detect pinned carrier threads while running your tests. Thus, you can eliminate one of the most prominent limitations or be aware of the problem.
要启用此检测:
To enable this detection:
-
1) Add the
junit5-virtual-threads
dependency to your project:
<dependency> <groupId>io.quarkus.junit5</groupId> <artifactId>junit5-virtual-threads</artifactId> <scope>test</scope> </dependency>
-
2) In your test case, add the
io.quarkus.test.junit5.virtual.VirtualThreadUnit
andio.quarkus.test.junit.virtual.ShouldNotPin
annotations:
@QuarkusTest @TestMethodOrder(MethodOrderer.OrderAnnotation.class) @VirtualThreadUnit // Use the extension @ShouldNotPin // Detect pinned carrier thread class TodoResourceTest { // ... }
当您运行测试(请记住使用 Java 21+)时,Quarkus 会检测已固定的载体线程。当出现这种情况时,测试失败。
When you run your test (remember to use Java 21+), Quarkus detects pinned carrier threads. When it happens, the test fails.
@ShouldNotPin
也可以直接用于方法。
The @ShouldNotPin
can also be used on methods directly.
junit5-virtual-threads 还提供了一个 @ShouldPin
批注,用于无法避免固定的情况。以下代码段演示了 @ShouldPin
批注用法。
The junit5-virtual-threads also provides a @ShouldPin
annotation for cases where pinning is unavoidable.
The following snippet demonstrates the @ShouldPin
annotation usage.
@VirtualThreadUnit // Use the extension
public class LoomUnitExampleTest {
CodeUnderTest codeUnderTest = new CodeUnderTest();
@Test
@ShouldNotPin
public void testThatShouldNotPin() {
// ...
}
@Test
@ShouldPin(atMost = 1)
public void testThatShouldPinAtMostOnce() {
codeUnderTest.pin();
}
}