Voltar para o inícioJava & Spring Boot

Como implementar Virtual Threads no Spring Boot em APIs com alta concorrência: guia prático para produção

Aprenda a implementar Virtual Threads no Spring Boot para APIs com alta concorrência, configurando Java 21, limites de dependências, testes de carga e observabilidade para produção.

M

Max Alex

Como implementar Virtual Threads no Spring Boot em APIs com alta concorrência: guia prático para produção

APIs que dependem de banco de dados, serviços HTTP e mensageria frequentemente acumulam requisições esperando I/O. Quando cada espera ocupa uma thread tradicional, os pools se esgotam rapidamente durante picos. As Virtual Threads oferecem uma forma mais simples de ampliar a concorrência em aplicações Java imperativas, desde que os limites das dependências continuem protegidos.

Neste guia, você verá como implementar Virtual Threads no Spring Boot, quais cenários se beneficiam, como evitar gargalos transferidos para banco e integrações e como validar a mudança antes de levá-la à produção.

O que são Virtual Threads e por que elas mudam a concorrência em Java

Virtual Threads são threads leves gerenciadas pela JVM, disponibilizadas de forma estável no Java 21 como parte do Project Loom. Elas permitem que uma aplicação mantenha muitas operações concorrentes, especialmente quando essas operações passam boa parte do tempo aguardando respostas externas.

No modelo tradicional, uma requisição bloqueada em uma chamada HTTP ou consulta JDBC mantém uma thread de plataforma ocupada. Com Virtual Threads, a JVM pode suspender a tarefa durante uma espera compatível e liberar a thread de plataforma para executar outro trabalho.

Isso preserva o modelo imperativo familiar: código sequencial, tratamento de exceções convencional e fluxo thread-por-request. Em vez de reestruturar toda a API para callbacks ou programação reativa, a equipe pode aumentar a concorrência com uma mudança mais incremental.

Porém, Virtual Threads não criam capacidade infinita. Se mil requisições chegarem ao mesmo tempo e todas precisarem de uma conexão com um banco capaz de atender apenas um conjunto menor de operações, o banco continuará sendo o limite. A melhoria está em não desperdiçar threads de plataforma enquanto as requisições aguardam I/O.

Por exemplo, uma API de checkout que consulta estoque e gateway de pagamento pode manter muitas solicitações em andamento sem reservar uma thread tradicional para cada espera de rede. O ganho real dependerá da latência, dos limites das dependências e da qualidade dos timeouts.

Quando usar Virtual Threads em APIs Spring Boot

O melhor cenário para Java Virtual Threads é uma API com alta concorrência e operações predominantemente bloqueantes: acesso JDBC, chamadas HTTP síncronas, leitura de arquivos, integrações com filas e outros recursos de rede.

Endpoints que consultam catálogo, preço e disponibilidade em sistemas externos são candidatos comuns. Em geral, eles passam mais tempo esperando respostas do que consumindo CPU. Nesses fluxos, ampliar o número de requisições que podem permanecer em espera tende a melhorar a utilização do servidor.

Já workloads limitados por CPU exigem outra abordagem. Processamento de vídeo, compressão intensa, criptografia pesada e cálculos complexos continuam precisando de limites de paralelismo adequados ao número de núcleos disponíveis. Criar muitas tarefas de CPU não acelera o processamento e pode aumentar contenção e latência.

Também não é obrigatório migrar uma aplicação Spring MVC madura para WebFlux apenas para atender mais conexões concorrentes. Virtual Threads podem ser uma alternativa pragmática para manter o modelo imperativo quando as bibliotecas e integrações usadas são bloqueantes.

A decisão deve partir de medições: identifique se a maior parte do tempo de resposta está em espera por I/O, avalie a saúde das dependências e compare a solução com a arquitetura atual sob carga representativa.

Pré-requisitos para adotar Virtual Threads no Spring Boot

Comece usando Java 21 ou superior, pois essa é a base estável para Virtual Threads. Também é importante utilizar uma versão atual do Spring Boot compatível com esse runtime e revisar a imagem de container usada no deploy.

Antes de ativar a funcionalidade, atualize drivers JDBC, clientes HTTP, bibliotecas de tracing e agentes de observabilidade. Componentes desatualizados podem dificultar diagnósticos ou introduzir comportamentos inesperados em cenários muito concorrentes.

Revise ainda os pontos que usam ThreadLocal, executores customizados, pools explícitos e sincronização. O uso de contexto por thread não é proibido, mas deve ser compreendido: uma aplicação pode criar muito mais threads lógicas do que no modelo tradicional.

Faça a primeira validação no mesmo tipo de ambiente que será usado em produção: mesma versão de Java, limites de memória próximos, configuração de rede semelhante e dependências com comportamento representativo. Comparações feitas apenas no notebook tendem a esconder gargalos importantes.

Como ativar Virtual Threads no Spring Boot

Em aplicações Spring Boot compatíveis executadas com Java 21, a configuração básica é habilitar a propriedade abaixo no arquivo de configuração.

spring:
  threads:
    virtual:
      enabled: true

A mesma configuração em application.properties seria:

spring.threads.virtual.enabled=true

Com essa opção, o Spring Boot passa a configurar componentes compatíveis para o uso de Virtual Threads, incluindo o processamento de requisições nos servidores suportados e executores auto-configurados quando aplicável. Ainda assim, não presuma que todo executor criado manualmente pela aplicação será alterado automaticamente.

Quando houver uma necessidade explícita de executar tarefas independentes, use um executor por tarefa baseado em Virtual Threads em vez de criar um pool fixo sem necessidade:

@Bean(destroyMethod = "close")
ExecutorService virtualThreadExecutor() {
    return Executors.newVirtualThreadPerTaskExecutor();
}

Evite substituir executores existentes sem entender quem os utiliza. Algumas tarefas devem continuar limitadas, especialmente aquelas intensivas em CPU, com acesso a recursos escassos ou capazes de disparar grande volume de chamadas externas.

Valide a ativação em runtime por meio de logs, thread dumps e métricas. Em código de diagnóstico controlado, Thread.currentThread().isVirtual() informa se a execução atual ocorre em uma Virtual Thread. Essa verificação é útil para testes, não como mecanismo de decisão de negócio.

Ajuste banco de dados, clientes HTTP e recursos compartilhados

O principal cuidado operacional é não confundir maior concorrência na aplicação com maior capacidade nas dependências. Virtual Threads podem permitir que mais requisições cheguem simultaneamente ao banco ou a um serviço externo; por isso, limites explícitos se tornam ainda mais importantes.

Dimensione o pool JDBC pela capacidade do banco

O pool de conexões JDBC deve refletir a capacidade medida do banco, as consultas executadas e os recursos disponíveis. Não dimensione o HikariCP pelo número potencial de Virtual Threads. Um pool excessivo pode aumentar a contenção no banco e piorar a latência de toda a API.

Defina tempos máximos para adquirir conexão, executar consultas e retornar respostas. Quando o pool estiver saturado, a aplicação deve falhar de modo controlado ou degradar uma funcionalidade não essencial, em vez de manter requisições aguardando indefinidamente.

Limite chamadas HTTP e defina timeouts

Clientes HTTP também precisam de limites de conexão, timeout de conexão, timeout de leitura e timeout total coerentes com o contrato da API consumida. Sem esses controles, uma dependência lenta pode reter um número muito grande de requisições concorrentes.

Para integrações críticas, combine timeout, limite de concorrência, circuit breaker e respostas de fallback quando fizer sentido para o negócio. A meta não é aceitar espera ilimitada: é usar a capacidade disponível de forma previsível.

Esse princípio vale para filas, caches, serviços de autenticação e armazenamento de arquivos. Cada recurso compartilhado precisa de uma política clara de capacidade, fila e falha.

Cuidados com pinning, sincronização e bibliotecas legadas

Virtual Threads funcionam melhor quando podem ser desmontadas da thread de plataforma enquanto aguardam I/O. Algumas situações podem reduzir esse benefício, como bloqueios prolongados dentro de trechos sincronizados, chamadas nativas e bibliotecas que mantêm uma Virtual Thread presa durante uma operação bloqueante.

Um caso clássico é um método synchronized que faz uma chamada remota. A seção crítica deveria proteger somente o estado compartilhado; a chamada HTTP, que pode demorar, deve ocorrer fora do bloqueio sempre que a consistência do domínio permitir.

public Response carregarDados(String id) {
    Estado estado;
    synchronized (monitor) {
        estado = cache.get(id);
    }
    return clienteExterno.buscar(estado);
}

Não substitua todos os usos de synchronized por locks automaticamente. A troca precisa ser motivada por profiling, traces ou thread dumps que indiquem contenção relevante. A prioridade é reduzir o tempo da seção crítica e eliminar I/O desnecessário dentro dela.

Inclua bibliotecas legadas, agentes de instrumentação e drivers na investigação. Execute testes de carga com observabilidade ativa para identificar pontos em que a escalabilidade esperada não aparece.

Como testar carga e comparar resultados antes do deploy

A adoção deve ser comprovada com um experimento controlado. Prepare dois ambientes equivalentes, alterando apenas a configuração de Virtual Threads, e aplique o mesmo roteiro de requisições após um período de aquecimento.

Estruture testes de carga para APIs com aumento progressivo de concorrência, distribuição de endpoints parecida com a produção e latências realistas nas dependências. Se possível, simule também falhas parciais e respostas lentas de serviços externos.

Compare, no mínimo, throughput, latência p50, p95 e p99, taxa de erro, uso de CPU, heap, pausas de coleta de lixo, conexões ativas e tempo de espera em pools. Métricas por endpoint são mais úteis do que médias globais, pois um fluxo específico pode concentrar a degradação.

Considere o teste bem-sucedido apenas se a aplicação mantiver ou melhorar seus indicadores sem sobrecarregar banco, cache ou serviços integrados. Uma API mais rápida que aumenta timeouts no serviço de pagamento não representa ganho operacional.

Documente a configuração, a versão do runtime, a massa de dados e os limites usados no experimento. Essa referência será essencial para reproduzir resultados e investigar mudanças futuras.

Observabilidade e operação de Virtual Threads em produção

Em produção, a contagem de threads isoladamente não explica a saúde da API. O monitoramento deve priorizar a experiência do usuário e a saturação das dependências: latência por endpoint, respostas 4xx e 5xx, timeouts, tempo de chamadas externas e ocupação de pools.

Use métricas do Spring Boot Actuator e do pool de conexões para acompanhar requisições, conexões ativas, conexões pendentes e erros. Traces distribuídos ajudam a identificar qual chamada está retendo uma requisição e tornam mais simples separar um problema da aplicação de uma lentidão externa.

Um painel útil relaciona o p95 dos endpoints com conexões do banco, duração de chamadas HTTP e taxa de erro. Se a latência crescer ao mesmo tempo que o pool JDBC atinge o limite, o diagnóstico aponta para capacidade ou eficiência do banco, não para falta de threads na API.

Crie alertas para aumento de timeouts, degradação sustentada dos percentis de latência, exaustão de conexões e elevação de respostas 5xx. Estabeleça limites com base no comportamento normal observado, evitando alertas genéricos que só geram ruído.

Checklist de rollout seguro para Virtual Threads

  • Confirme Java 21 ou superior e uma versão compatível do Spring Boot.
  • Comece por endpoints com espera predominante por I/O e métricas já conhecidas.
  • Habilite spring.threads.virtual.enabled=true em ambiente controlado.
  • Revise pool JDBC, limites de clientes HTTP, timeouts e políticas de degradação.
  • Investigue sincronização longa, código nativo e bibliotecas legadas sob carga.
  • Compare cenários equivalentes com threads de plataforma e Virtual Threads.
  • Monitore percentis de latência, erros, CPU, heap e saúde das dependências.
  • Faça rollout gradual por canário, percentual de tráfego ou feature flag.
  • Defina critérios objetivos de rollback antes da liberação.

Uma implantação segura não depende apenas de ativar uma propriedade. Ela combina configuração correta, limites de capacidade, testes realistas e telemetria suficiente para corrigir rapidamente qualquer regressão.

Sua API sofre com picos de concorrência, latência em integrações ou exaustão de threads? Avalie a arquitetura atual e planeje uma adoção segura de Virtual Threads com métricas, testes de carga e controles de capacidade desde o início.

M

Max Alex

Criador de conteúdo apaixonado por tecnologia e inovação. Acompanhe nossos artigos para ficar por dentro das melhores estratégias digitais.

Continue lendo