Referência de apoio para Threads Virtuais
Este guia explica como se beneficiar das threads virtuais do Java 21+ no aplicativo Quarkus.
O que são threads virtuais?
Terminologia
- Thread do SO
-
Uma estrutura de dados "semelhante à uma thread" gerenciada pelo sistema operacional.
- Thread de plataforma
-
Até o Java 19, cada instância da classe Thread era uma thread de plataforma, um invólucro em torno de uma thread de sistema operacional. A criação de uma thread de plataforma cria uma thread do sistema operacional, e o bloqueio de uma thread de plataforma bloqueia uma thread do sistema operacional.
- Thread virtual
-
Threads leves e gerenciadas pela JVM. Eles estendem a classe Thread , mas não estão vinculadas a uma thread específica do sistema operacional. Portanto, o agendamento de threads virtuais é de responsabilidade da JVM.
- Carrier thread
-
Uma thread de plataforma usada para executar uma thread virtual é chamada de thread de transporte . Não se trata de uma classe distinta da Thread ou
VirtualThread, mas sim de uma denominação funcional.
Diferenças entre threads virtuais e threads de plataforma
Faremos aqui um breve resumo do tema; para mais informações, consulte o JEP 425.
As threads virtuais são um recurso disponível desde o Java 19 (o Java 21 é a primeira versão LTS que inclui threads virtuais), com o objetivo de fornecer uma alternativa barata às threads de plataforma para cargas de trabalho vinculadas a E/S.
Até agora, as threads de plataforma eram a unidade de simultaneidade da JVM. Elas são um invólucro sobre as estruturas do sistema operacional. A criação de uma thread de plataforma Java cria uma estrutura "semelhante a uma thread" em seu sistema operacional.
As threads virtuais, por outro lado, são gerenciadas pela JVM. Para serem executadas, elas precisam ser montadas em uma thread de plataforma (que atua como uma portadora para essa thread virtual). Dessa forma, elas foram projetadas para oferecer as seguintes características:
- Leve
-
As threads virtuais ocupam menos espaço na memória do que as threads de plataforma. Portanto, é possível usar mais threads virtuais do que threads de plataforma simultaneamente sem estourar a memória. Por padrão, as threads de plataforma são criadas com uma pilha de cerca de 1 MB, enquanto a pilha de threads virtuais é "paga conforme o uso". Você pode encontrar esses números e outras motivações para as threads virtuais nesta apresentação feita pelo desenvolvedor líder do projeto Loom (o projeto que adicionou o suporte a threads virtuais à JVM).
- Barato para criar
-
Criar uma thread de plataforma em Java leva tempo. Atualmente, técnicas como o pooling, em que as threads são criadas uma vez e depois reutilizados, são fortemente incentivadas para minimizar o tempo perdido ao iniciá-los (além de limitar o número máximo de threads para manter o consumo de memória baixo). Supõe-se que as threads virtuais sejam entidades descartáveis que criamos quando precisamos delas; não é recomendável agrupá-las ou reutilizá-las para tarefas diferentes.
- Barato para bloquear
-
Ao executar o bloqueio de E/S, a thread do sistema operacional subjacente envolvido pela thread da plataforma Java é colocada em uma fila de espera e ocorre uma troca de contexto para carregar um novo contexto de thread no núcleo da CPU. Essa operação leva tempo. Como a JVM gerencia threads virtuais, nenhuma thread do sistema operacional subjacente é bloqueada quando executa uma operação de bloqueio. Seu estado é armazenado no heap, e outra thread virtual é executada na mesma thread da plataforma Java (portadora).
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
Agora sabemos que podemos criar mais threads virtuais do que threads de plataforma. Você pode se sentir tentado a usar threads virtuais para realizar cálculos longos (carga de trabalho vinculada à CPU). Isso é inútil e contraproducente. O CPU-bound não consiste em trocar rapidamente as threads enquanto elas precisam aguardar a conclusão de uma E/S, mas em deixá-las conectadas a um núcleo da CPU para computar algo. Nesse cenário, é pior do que inútil ter milhares de threads se tivermos dezenas de núcleos de CPU, as threads virtuais não melhorarão o desempenho das cargas de trabalho vinculadas à CPU. Pior ainda, ao executar uma carga de trabalho vinculada à CPU em uma thread virtual, a thread virtual monopoliza a thread de suporte na qual está montada. Ele reduzirá a chance de execução da outra thread virtual ou começará a criar novas threads de suporte, o que levará a um alto uso da memória.
Execute código em threads virtuais usando @RunOnVirtualThread
No Quarkus, o suporte a thread virtual é implementado usando a anotação @RunOnVirtualThread . Esta seção apresenta uma breve visão geral da lógica e de como usá-la. Há guias dedicados para extensões que oferecem suporte a essa anotação, como:
Por que não executar tudo em threads virtuais?
Conforme mencionado acima, nem tudo pode ser executado com segurança em threads virtuais. O risco de monopolização pode levar a um alto uso da memória. Além disso, há situações em que a thread virtual não pode ser desmontada da thread de suporte. Isso é chamado de pinning . Por fim, algumas bibliotecas usam ThreadLocal para armazenar e reutilizar objetos. O uso de threads virtuais com essas bibliotecas levará a uma alocação maciça, pois os objetos intencionalmente agrupados serão instanciados para cada thread virtual (descartável e geralmente de curta duração).
Até o momento, não é possível usar threads virtuais de forma despreocupada. Seguir essa abordagem laissez-faire poderia levar rapidamente a problemas de falta de memória e de recursos. Assim, o Quarkus usa um modelo explícito até que os problemas mencionados acima desapareçam (à medida que o ecossistema Java amadurece). Esse também é o motivo pelo qual as extensões reativas têm o suporte a threads virtuais, e raramente as clássicas . Precisamos saber quando despachar em uma thread virtual.
É 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
A monopolização foi explicada na seção As threads virtuais são úteis apenas para cargas de trabalho vinculadas a E/S . Ao executar cálculos longos, não permitimos que a JVM desmonte e mude para outra thread virtual até que a thread virtual termine. De fato, o agendador atual não oferece suporte à antecipação de tarefas.
Essa monopolização pode levar à criação de novas threads de suporte para executar outras threads virtuais. A criação de threads de suporte resulta na criação de threads de plataforma. Portanto, há um custo de memória associado a essa criação.
Suponha que você execute em um ambiente restrito, como os contêineres. Nesse caso, a monopolização pode se tornar rapidamente uma preocupação, pois o alto uso da memória pode levar a problemas de falta de memória e ao encerramento do contêiner. O uso da memória pode ser maior do que com threads de trabalho regulares devido ao custo inerente do agendamento e das threads virtuais.
Casos de fixação (pinning)
A promessa de "bloqueio barato" pode nem sempre ser cumprida: uma thread virtual pode fixar seu portador em determinadas ocasiões. A thread da plataforma é bloqueada nessa situação, exatamente como seria em um cenário de bloqueio típico.
Java 24+ Improvement: Starting with Java 24, thanks to JEP 491: Synchronize Virtual Threads without Pinning, synchronized blocks and methods no longer cause pinning.
This is a major improvement that eliminates one of the most common pinning scenarios.
However, native code interactions can still cause pinning (see below).
|
According to JEP 425, pinning can happen in the following situations:
On Java 21-23:
-
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
On Java 24+:
-
when a virtual thread calls native code (via JNI or the Foreign Function & Memory API) and that native code calls back to Java code that performs a blocking operation
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 on Java 21-23. On Java 24+, most JDBC drivers no longer pin the carrier thread due to the synchronized improvements.
Para obter mais informações, consulte Quando o Quarkus encontra Virtual Threads
| This information about pinning cases applies to PostgreSQL JDBC driver 42.5.4 and earlier on Java 21-23. 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)
Algumas bibliotecas estão usando o ThreadLocal como um mecanismo de pool de objetos. Bibliotecas extremamente populares, como Jackson e Netty, presumem que o aplicativo usa um número limitado de threads, que são recicladas (usando um pool de threads) para executar várias tarefas (não relacionadas, mas sequenciais).
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.
No entanto, esse padrão é contraproducente ao usar threads virtuais. As threads virtuais não são agrupadas e geralmente têm vida curta. Portanto, em vez de algumas delas, agora temos muitas. Para cada uma delas, o objeto armazenado no ThreadLocal é criado (geralmente grande e caro) e não será reutilizado, pois a thread virtual não é agrupada (e não será usada para executar outra tarefa após a conclusão da execução). Esse problema leva a um alto uso de memória. Infelizmente, ele exige alterações sofisticadas no código das próprias bibliotecas.
Use @RunOnVirtualThread with Quarkus REST (formerly RESTEasy Reactive)
Esta seção mostra um breve exemplo de uso da anotação @RunOnVirtualThread . Ela também explica os vários modelos de desenvolvimento e execução oferecidos pelo Quarkus.
A anotação @RunOnVirtualThread instrui o Quarkus a invocar o método anotado em uma nova thread virtual em vez da atual. O Quarkus lida com a criação da thread virtual e com o descarregamento.
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).
Para isso, basta adicionar a anotação @RunOnVirtualThread ao endpoint. Se a máquina virtual Java usada para executar o aplicativo oferecer suporte a thread virtual (portanto, Java 21 ou versões posteriores), a execução do endpoint será transferida para uma thread virtual. Assim, será possível executar operações de bloqueio sem bloquear a thread da plataforma no qual a thread virtual está montada.
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")
Em seguida, você também precisa se certificar de que está usando o Java 21+, o que pode ser imposto no arquivo pom.xml com o seguinte:
<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
</properties>
Três modelos de desenvolvimento e execução
O exemplo abaixo mostra as diferenças entre três pontos de extremidade, todos eles consultando uma fortuna no banco de dados e retornando-a ao cliente.
-
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.
-
O terceiro usa o Mutiny, mas de forma síncrona. Como ele não retorna um "tipo reativo", é considerado bloqueador e a anotação @RunOnVirtualThread pode ser usada.
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
Conforme mencionado na seção Por que não executar tudo em threads virtuais? o ecossistema Java não está totalmente pronto para threads virtuais. Portanto, você precisa ter cuidado, especialmente ao usar bibliotecas que fazem E/S.
Felizmente, o Quarkus oferece um enorme ecossistema que está pronto para ser usado em threads virtuais. Mutiny, a biblioteca de programação reativa usada no Quarkus, e as ligações Vert.x Mutiny oferecem a capacidade de escrever código de bloqueio (portanto, sem medo, sem curva de aprendizado) que não fixa a thread portadora.
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
Unipode 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
Java 24+ Change: The -Djdk.tracePinnedThreads system property is no longer available starting with Java 24. This flag has been removed as part of the virtual thread improvements introduced in JEP 491. For Java 24+, use the JFR (Java Flight Recorder) events or the Quarkus testing extensions described below to detect pinning.
|
For Java 21-23 only:
We recommend using the following configuration when running tests in applications using virtual threads. It will not fail the tests, but at least dump stack 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> <!-- Only works on Java 21-23 -->
</configuration>
</plugin>
For all Java versions (21+):
For a more robust approach that works on all Java versions, use the junit-virtual-threads extension described in the Testing virtual thread applications section below. This extension uses JFR events to detect pinning and works consistently across Java 21, 24, and later versions.
Executar a aplicação utilizando threads virtuais
java -jar target/quarkus-app/quarkus-run.jar
Antes do Java 21, as threads virtuais ainda eram um recurso experimental; você precisa iniciar seu aplicativo com o sinalizador --enable-preview .
|
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.
Nesta seção, usamos o JIB para criar o contêiner. Consulte o guia de conteinerização para saber mais sobre as alternativas.
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/ubi9/openjdk-21-runtime (1)
quarkus.jib.platforms=linux/amd64,linux/arm64 (2)
| 1 | Certifique-se de usar uma imagem de base que ofereça suporte a threads virtuais. Aqui, usamos uma imagem que fornece o Java 21. O Quarkus seleciona automaticamente uma imagem que fornece Java 21+ se você não definir uma. |
| 2 | Selecione a arquitetura de destino. Você pode selecionar mais do que uma para criar imagens multiarquitetura. |
Em seguida, construa o contêiner como faria normalmente. Por exemplo, se estiver usando o Maven, execute:
mvn package
Compilando a aplicação Quarkus utilizando threads virtuais num executável nativo
Usando uma instalação local do GraalVM
Para compilar um aplicativo Quarkus que utiliza a @RunOnVirtualThread em um executável nativo, você deve usar um GraalVM/Mandrel native-image com suporte a threads virtuais, fornecendo, portanto, pelo menos o Java 21.
Construa o executável nativo conforme indicado no guia de compilação nativa . Por exemplo, com o Maven, execute:
mvn package -Dnative
Usando uma compilação em contêiner
O build em contêiner permite a criação de executáveis Linux 64 usando um compilador native-image executado em um contêiner. Isso evita a necessidade de instalar o native-image em seu computador e também permite configurar a versão do GraalVM de que você precisa. Observe que, para usar o build em contêiner, é necessário ter o Docker ou o Podman instalado em seu computador.
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
Se estiver usando um Mac M1 ou M2 (usando uma CPU ARM64), você precisa estar ciente de que o executável nativo que obterá usando uma compilação no contêiner será um executável Linux, mas usando a arquitetura do host (ARM 64). Você pode usar a emulação para forçar a arquitetura ao usar o Docker com a seguinte propriedade:
Fique ciente de que isso aumenta significativamente o tempo de compilação… bastante (>10 minutos). |
Conteinerização de aplicações nativas usando threads virtuais
Para criar um contêiner que execute um aplicativo Quarkus usando threads virtuais compiladas em um executável nativo, é preciso garantir que o usuário tenha um executável Linux/AMD64 (ou ARM64, se estiver direcionado a máquinas ARM).
Certifique-se de que o seu application.properties contém a configuração explicada na seção de compilação nativa.
Em seguida, construa o contêiner como faria normalmente. Por exemplo, se estiver usando o Maven, execute:
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
Os métodos anotados com @RunOnVirtualThread herdam do contexto duplicado original (consulte o guia de referência de contexto duplicado para obter detalhes). Portanto, os dados gravados no contexto duplicado (e o escopo da solicitação, já que o escopo da solicitação é armazenado no contexto duplicado) por filtros e interceptores estão disponíveis durante a execução do método (mesmo que os filtros e interceptores não sejam executados na thread virtual).
No entanto, as thread locals não são propagadas.
Nomes de threads virtuais
As threads virtuais são criadas sem um nome de thread por padrão, o que não é prático para identificar a execução para fins de depuração e registro. As threads virtuais gerenciadas pelo Quarkus são nomeadas e prefixadas com quarkus-virtual-thread- . É possível personalizar esse prefixo ou desativar a nomeação configurando um valor vazio:
quarkus.virtual-threads.name-prefix=
Injetar o executor de thread virtual
Para executar tarefas em threads virtuais, o Quarkus gerencia um ThreadPerTaskExecutor interno. Nos raros casos em que você precisa acessar esse executor diretamente, pode injetá-lo usando o qualificador @VirtualThreads CDI:
| A injeção da Virtual Thread ExecutorService é experimental e pode ser alterada em versões futuras. |
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());
}
}
Teste de aplicativos de thread virtual
As mentioned above, virtual threads have a few limitations that can drastically affect your application performance and memory usage. The junit-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.
Para ativar essa detecção:
-
1) Add the
junit-virtual-threadsdependency to your project:
<dependency>
<groupId>io.quarkus.junit</groupId>
<artifactId>junit-virtual-threads</artifactId>
<scope>test</scope>
</dependency>
-
2) In your test case, add the
io.quarkus.test.junit.virtual.VirtualThreadUnitandio.quarkus.test.junit.virtual.ShouldNotPinannotations:
@QuarkusTest
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@VirtualThreadUnit // Use the extension
@ShouldNotPin // Detect pinned carrier thread
class TodoResourceTest {
// ...
}
Quando você executa o teste (lembre-se de usar Java 21+), o Quarkus detecta threads de suporte fixadas. Quando isso acontece, o teste falha.
A anotação @ShouldNotPin também pode ser usado diretamente nos métodos.
The junit-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();
}
}
Virtual thread metrics
You can enable the Micrometer Virtual Thread binder by adding the following artifact to your application:
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-java21</artifactId>
</dependency>
This binder keeps track of the number of pinning events and the number of virtual threads failed to be started or un-parked. See the MicroMeter documentation for more information.
You can explicitly disable the binder by setting the following property in your application.properties:
# The binder is automatically enabled if the micrometer-java21 dependency is present
quarkus.micrometer.binder.virtual-threads.enabled=false
In addition, if the application is running on a JVM that does not support virtual threads (prior to Java 21), the binder is automatically disabled.
You can associate tags to the collected metrics by setting the following properties in your application.properties:
quarkus.micrometer.binder.virtual-threads.tags=tag_1=value_1, tag_2=value_2