Referência de apoio para Threads Virtuais
This guide explains how to benefit from Java 21+ virtual threads in Quarkus application.
O que são threads virtuais?
Terminologia
- Thread do SO
-
Uma estrutura de dados "semelhante à uma thread" gerenciada pelo sistema operacional.
- Thread de plataforma
-
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.
- Thread virtual
-
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.
Diferenças entre threads virtuais e threads de plataforma
Faremos aqui um breve resumo do tema; para mais informações, consulte o JEP 425.
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.
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.
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:
- Leve
-
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).
- Barato para criar
-
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.
- Barato para bloquear
-
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.
O Processo de Continuação
Como mencionado acima, a JVM agenda as threads virtuais. Essas threads virtuais são montadas em carrier threads. O agendamento vem com uma pitada de magia. Quando a thread virtual tenta utilizar E/S bloqueante, a JVM transforma esta chamada numa chamada não bloqueante, desmonta a thread virtual e monta outra thread virtual na carrier thread. Quando a E/S é concluída, a thread virtual em espera torna-se novamente elegível e será montada novamente numa carrier thread para continuar a sua execução. Para o usuário, todo esse processo é invisível. O seu código síncrono é executado de forma assíncrona.
Note-se que a thread virtual pode não ser montada novamente na mesma carrier thread.
As threads virtuais são úteis apenas para cargas de trabalho vinculadas a E/S
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.
Execute código em threads virtuais usando @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:
Por que não executar tudo em threads virtuais?
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.
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.
É essencial compreender que estes problemas não são limitações ou bugs do Quarkus, mas sim devidos ao estado atual do ecossistema Java, que precisa evoluir para se tornar compatível com as threads virtuais.
Para saber mais sobre o design interno e as escolhas, consulte o documento Considerações sobre a integração de threads virtuais numa estrutura Java: um exemplo Quarkus num ambiente com recursos limitados. |
Casos de monopolização
The monopolization has been explained in the Virtual threads are useful for I/O-bound workloads only 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.
Casos de fixação (pinning)
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.
Segundo o JEP 425, isto pode acontecer em duas situações:
-
quando uma thread virtual executa uma operação de bloqueio dentro de um bloco ou método
synchronized
-
quando executa uma operação bloqueante dentro de um método nativo ou de uma função externa
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.
For more information, see When Quarkus meets Virtual Threads
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. |
O caso do agrupamento (pooling)
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.
Este padrão tem várias vantagens, tais como:
-
Vantagem da alocação: objetos pesados só são atribuídos uma vez por thread, mas como o número destas threads seria limitado, não consumiria memória excessivamente.
-
Segurança da thread: apenas uma thread pode acessar o objeto armazenado na thread local - prevenindo acessos concorrentes.
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)
This section shows a brief example of using the @RunOnVirtualThread annotation. It also explains the various development and execution models offered by 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.
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).
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.
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
Adicione a seguinte dependência ao seu arquivo de build:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest</artifactId>
</dependency>
implementation("io.quarkus:quarkus-rest")
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>
Três modelos de desenvolvimento e execução
The example below shows the differences between three endpoints, all of them querying a fortune in the database then returning it to the client.
-
o primeiro utiliza o estilo de bloqueio tradicional, que é considerado bloqueante devido à sua assinatura.
-
o segundo utiliza o Mutiny, que é considerado não bloqueante devido à sua assinatura.
-
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);
}
}
A tabela seguinte resume as opções:
Modelo | Exemplo de assinatura | Prós | Contras |
---|---|---|---|
Código síncrono na worker thread |
|
Código simples |
Utiliza worker thread (limita a concorrência) |
Código reativo no event loop |
|
Alta concorrência e baixo uso de recursos |
Código mais complexo |
Código síncrono em thread virtual |
|
Código simples |
Risco de fixação (pinning), monopolização e agrupamento de objetos pouco eficiente |
Note-se que os três modelos podem ser utilizados numa única aplicação.
Utilize clientes compatíveis com threads virtuais
As mentioned in the Why not run everything on virtual threads? 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.
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.
Como resultado:
-
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…
-
A API que retornar
Uni
pode ser utilizada diretamente através deuni.await().atMost(…)
. Bloqueia a thread virtual, sem bloquear a carrier thread, e também melhora a resiliência da sua aplicação com um suporte fácil de timeout (não bloqueante). -
Se você estiver utilizando um cliente Vert.x que utilize as ligações Mutiny, use os métodos
andAwait()
que bloqueiam até obter o resultado sem fixar a carrier thread. Isso inclui todos os drivers SQL reativos.
Detectar thread fixada em testes
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>
Executar a aplicação utilizando threads virtuais
java -jar target/quarkus-app/quarkus-run.jar
Prior to Java 21, virtual threads were still an experimental feature, you need to start your application with the --enable-preview flag.
|
Construir contêineres para aplicação que utiliza threads virtuais
Ao executar a sua aplicação em modo JVM (portanto, não compilada em modo nativo, para nativo consulte a seção dedicada), pode seguir o guia de conteinerização para construir um contêiner.
In this section, we use JIB to build the container. Refer to the containerization guide to learn more about the alternatives.
Para conteinerizar a sua aplicação Quarkus que utiliza @RunOnVirtualThread
, adicione as seguintes propriedades ao seu 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 | Selecione a arquitetura de destino. Você pode selecionar mais do que uma para criar imagens multiarquitetura. |
Then, build your container as you would do usually. For example, if you are using Maven, run:
mvn package
Compilando a aplicação Quarkus utilizando threads virtuais num executável nativo
Usando uma instalação local do GraalVM
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.
Build the native executable as indicated on the native compilation guide. For example, with Maven, run:
mvn package -Dnative
Usando uma compilação em contêiner
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.
Em seguida, adicione ao seu arquivo application.properties
:
# In-container build to get a linux 64 executable
quarkus.native.container-build=true (1)
1 | Permite a compilação no contêiner |
De ARM/64 para AMD/64
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:
Fique ciente de que isso aumenta significativamente o tempo de compilação… bastante (>10 minutos). |
Conteinerização de aplicações nativas usando threads virtuais
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).
Certifique-se de que o seu application.properties
contém a configuração explicada na seção de compilação nativa.
Then, build your container as you would do usually. For example, if you are using Maven, run:
mvn package -Dnative
Se você quiser construir uma imagem de contêiner nativa e já tiver uma imagem nativa existente, você pode definir -Dquarkus.native.reuse-existing=true . Isso evitará que a imagem nativa seja construída novamente
|
Utilize o contexto duplicado em threads virtuais
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).
No entanto, as thread locals não são propagadas.
Nomes de threads virtuais
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
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:
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
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 { // ... }
When you run your test (remember to use Java 21+), Quarkus detects pinned carrier threads. When it happens, the test fails.
The @ShouldNotPin
can also be used on methods directly.
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();
}
}