The English version of quarkus.io is the official project site. Translated sites are community supported on a best-effort basis.
Edit this Page

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.

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:

pom.xml
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-rest</artifactId>
</dependency>
build.gradle
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:

pom.xml
<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

Fortune blocking()

Código simples

Utiliza worker thread (limita a concorrência)

Código reativo no event loop

Uni<Fortune> reactive()

Alta concorrência e baixo uso de recursos

Código mais complexo

Código síncrono em thread virtual

@RunOnVirtualThread Fortune vt()

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:

  1. 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…​

  2. A API que retornar Uni pode ser utilizada diretamente através de uni.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).

  3. 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:

quarkus.native.container-runtime-options=--platform=linux/amd64

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-threads dependency 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.VirtualThreadUnit and io.quarkus.test.junit.virtual.ShouldNotPin annotations:

@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

Related content