Virtual Threads no Spring Boot: crie uma API concorrente passo a passo
Por John Pires · 19 de março de 2026

Virtual Threads no Spring Boot: crie uma API concorrente passo a passo
1) Por que aprender isso em 2026
Contexto técnico
Em 2026 virtual threads já estão maduros: JDK 21+ estabilizou Project Loom, e frameworks como Spring Boot adaptaram-se para tirar proveito dessa alternativa leve aos threads nativos (platform threads). Virtual threads reduzem o custo de criar milhares de threads para operações de I/O bloqueante, tornando designs antigos mais simples e seguros para cargas concorrentes elevadas.
Analogia simples
Pense em platform threads como carros grandes que consomem muita gasolina e virtual threads como bicicletas: uma bicicleta (virtual thread) é barata e rápida de encomendar quando precisa atravessar a cidade, mas ainda permite que você pare em frente a uma loja (bloqueio). A analogia ajuda iniciantes a entender por que criar 10.000 carros é inviável, mas 10.000 bicicletas é plausível.
Aprender virtual threads hoje significa entender como reescrever trechos bloqueantes sem migrar imediatamente para um modelo reativo (non-blocking). Em microservices com muitas integrações legadas (bancos, APIs externas), essa é uma ferramenta prática que simplifica código.
Do ponto de vista de arquitetura, virtual threads mudam trade-offs: menos complexidade em código, menos necessidade de callbacks/fluxos reativos, mas você precisa conhecer armadilhas como usar bibliotecas que assumem platform threads (ex.: certas implementações de thread-local).
2) Configuração do ambiente
Requisitos mínimos
Para seguir os exemplos use JDK 21+ (ou superior), Maven 3.8+, e Spring Boot 3.2+ (versão hipotética compatível com Loom; em 2026 as releases principais já oferecem suporte). IDE como IntelliJ IDEA ou VS Code ajudam, mas o exemplo roda via linha de comando.
Estrutura do projeto
Crie um projeto Spring Boot básico com dependências: spring-boot-starter-web, spring-boot-starter-data-jpa (se for usar banco), e opcionalmente spring-boot-starter-test. Para simular I/O, usaremos chamadas bloqueantes (Thread.sleep) em serviços: assim demonstramos ganho real com virtual threads.
Organização sugerida de pacotes: com.example.vthreaddemo com subpacotes controller, service, config, util. Código de exemplo a seguir assume Maven e empacotamento padrão do Spring Boot.
3) Hello World progressivo
O jeito errado (código de revisão)
Primeiro exemplo mostra uma prática ruim: criar platform threads por requisição. Em revisão de código recomendamos evitar criar threads manualmente dentro de endpoints — isso resulta em vazamentos e consumo descontrolado.
Contexto do bloco abaixo: é um Controller Spring Boot que cria um Thread manualmente para tratar a requisição. O código funciona, mas escala mal.
package com.example.vthreaddemo.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class BadController {
@GetMapping("/bad")
public String bad() throws InterruptedException {
Thread t = new Thread(() -> {
try {
// simula trabalho bloqueante
Thread.sleep(200);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
t.start();
// retorna imediatamente, mas o servidor criou uma platform thread por requisição
return "accepted";
}
}
O que acontece: esse código cria platform threads arbitrariamente. Em carga alta você abre milhares de threads nativas, consumindo memória e causando GC/oom. Em code review, apontamos: "não crie threads manualmente dentro de endpoints".
O jeito pior (executor sem limite)
Outro erro comum é usar um ExecutorService sem controle, por exemplo newCachedThreadPool. Funciona em poucos testes, mas explode sob carga.
package com.example.vthreaddemo.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@RestController
public class UglyExecutorController {
private final ExecutorService exec = Executors.newCachedThreadPool();
@GetMapping("/ugly")
public String ugly() {
exec.submit(() -> {
try { Thread.sleep(300); } catch (InterruptedException ignored) {}
});
return "accepted";
}
}
O que acontece: newCachedThreadPool cria threads nativas ilimitadas se necessário, causando os mesmos problemas. Em revisão: prefira executors dimensionados ou virtual threads.
Correto com virtual threads
Agora o código certo: forneça um ExecutorService baseado em virtual threads e reutilize-o como bean Spring. Isso permite criar milhares de tarefas bloqueantes com baixo custo.
Contexto: configuramos um bean ExecutorService usando Executors.newVirtualThreadPerTaskExecutor() e usamos CompletableFuture.supplyAsync para delegar trabalho bloqueante. Spring serializará a resposta corretamente quando retornar CompletableFuture.
package com.example.vthreaddemo.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@Configuration
public class VirtualThreadConfig {
@Bean(destroyMethod = "shutdown")
public ExecutorService virtualExecutor() {
// cria um executor que aloca virtual threads por tarefa
return Executors.newVirtualThreadPerTaskExecutor();
}
}
O que acontece: o bean cria um ExecutorService que usa virtual threads. Ele deve ser reutilizado em toda a aplicação para evitar criar pools desnecessários.
package com.example.vthreaddemo.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
@RestController
public class GoodController {
private final ExecutorService vExec;
public GoodController(ExecutorService vExec) {
this.vExec = vExec;
}
@GetMapping("/virtual")
public CompletableFuture<String> virtual() {
return CompletableFuture.supplyAsync(() -> {
try { Thread.sleep(200); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
return "done";
}, vExec);
}
}
O que acontece: cada requisição aciona uma virtual thread barata. Sob carga, a latência se mantém melhor sem esgotar memória com threads nativas.
4) Mini-projeto prático
Objetivo e requisitos
Vamos construir um micro serviço simples que recebe N requisições concorrentes que executam uma operação bloqueante simulada (chamada a um banco). O objetivo é comparar três abordagens: threads manuais, executor nativo ilimitado, e virtual threads.
Premissas: Spring Boot app com endpoints /bad, /ugly, /virtual. Uso de in-memory e simulação com Thread.sleep para I/O.
Implementação: serviço bloqueante simulado
Contexto: serviço que representa um repositório legado que bloqueia. É usado pelos controllers acima. O exemplo abaixo é uma classe utilitária que simula latência de I/O.
package com.example.vthreaddemo.service;
public class BlockingService {
public String query() {
// simula latência de banco
try {
Thread.sleep(150);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return "interrupted";
}
return "ok";
}
}
O que acontece: cada chamada bloqueia a thread atual por 150ms. Esse comportamento é comum em drivers JDBC clássicos.
Teste de carga simples com Java HttpClient
Contexto: carrego a API enviando 200 requisições concorrentes para comparar resultados. Esse snippet é um cliente Java que roda localmente para gerar carga.
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
public class LoadTest {
public static void main(String[] args) throws Exception {
HttpClient client = HttpClient.newHttpClient();
List<CompletableFuture<HttpResponse<String>>> list = new ArrayList<>();
for (int i = 0; i < 200; i++) {
HttpRequest req = HttpRequest.newBuilder()
.uri(new URI("http://localhost:8080/virtual"))
.GET()
.build();
list.add(client.sendAsync(req, HttpResponse.BodyHandlers.ofString()));
}
CompletableFuture.allOf(list.toArray(new CompletableFuture[0])).join();
System.out.println("done");
}
}
O que acontece: o cliente dispara 200 requisições paralelas. Compare memória, tempo de resposta e erros entre endpoints /bad, /ugly e /virtual para ver o impacto das escolhas.
Observações práticas
Ao executar o load test, observe o uso de memória, criação de threads e latência. Com virtual threads a aplicação costuma manter latência estável e usar menos heap para estado de thread.
Dica: Sempre reproduza cenários reais de I/O. Simular apenas CPU-bound não revela ganho de virtual threads.
5) Recursos para continuar estudando
Referências oficiais
Leia documentação do JDK sobre Project Loom, notas de release do JDK 21+ e guias do Spring Boot que mencionam virtual threads e suporte a executors. Esses recursos explicam internals e limitações.
Artigos e leitura adicional
Procure por posts técnicos que comparam virtual threads e modelos reativos, exemplos de bibliotecas ainda não compatíveis e benchmarks realistas. Repositórios no GitHub com exemplos práticos ajudam a entender trade-offs.
- Documentação JDK Project Loom
- Guia do Spring Boot sobre integração com executors
- Exemplos de benchmark e microservices
Erros comuns, armadilhas e dicas práticas
Erros comuns
1) Assumir que toda biblioteca é segura para virtual threads. Algumas usam APIs internas ou thread-local que supõem platform threads.
2) Criar executors virtual-thread por requisição em vez de reutilizar um bean global. Isso causa overhead na criação de pools e confunde lifecycle.
Dicas práticas
1) Reutilize um ExecutorService virtual como bean. Dê destroyMethod para shutdown.
2) Não misture blocking-code com frameworks que exigem non-blocking sem entender compatibilidade. Virtual threads permitem manter blocking-code sem reescrever, mas não resolvem bibliotecas que usam locks pesados.
Exemplo de revisão: "Substituir newCachedThreadPool por Executors.newVirtualThreadPerTaskExecutor() e expor como @Bean para reaproveitamento".
Conclusão prática
Virtual threads são uma ferramenta pragmática para modernizar serviços que ainda dependem de I/O bloqueante. Em code review, a recomendação é clara: não crie platform threads manualmente; prefira um Executor virtual bem gerenciado. Isso simplifica código e melhora escalabilidade sem a complexidade do modelo reativo.
Resumo das ações imediatas que você pode aplicar hoje:
- Atualize para JDK 21+ e Spring Boot compatível.
- Exponha um ExecutorService virtual como bean e reutilize-o.
- Substitua criadores de threads manuais e executors ilimitados por virtual threads.
- Teste com carga realista e monitore thread count, GC e latência.
Finalizando: a migração para virtual threads é um passo incremental e reversível. A estratégia recomendada é começar por endpoints que fazem chamadas bloqueantes e iterar: primeiro trocar o modelo de threads, depois avaliar possíveis refactors para alternativas reativas apenas quando realmente necessário.
Se precisar, forneço um repositório de exemplo com todo o código pronto para rodar e scripts de carga para comparar os endpoints. Em code review, a mudança é simples e de alto impacto: menos recursos, menos complexidade, mesma lógica.
Assuntos: Java, spring-boot, virtual-threads, loom, concurrency
Leia também
Virtual Threads no Spring Boot: migrar controllers bloqueantes, JPA e tuning do HikariCP
Guia prático para migrar controllers bloqueantes e JPA para virtual threads no Spring Boot, com exemplos reais, setup do HikariCP, armadilhas e padrões para…
Virtual Threads no Spring Boot: otimizar JDBC/Hikari em microservices
Guia prático para usar virtual threads em Spring Boot e otimizar JDBC/Hikari em microservices: corrija anti-patterns, configure Hikari corretamente e aplique…
Circuit Breaker com Resilience4j no Spring Boot: tutorial passo a passo
Aprenda passo a passo a proteger chamadas remotas em Spring Boot usando Resilience4j. Conceitos, configuração, código comentado, exemplos práticos e…
Spring Boot 3: habilitando Virtual Threads para APIs REST bloqueantes
Guia prático para habilitar virtual threads em APIs REST bloqueantes com Spring Boot 3: configuração do executor, exemplos com CompletableFuture…
Migrando endpoints bloqueantes para Virtual Threads no Spring Boot (passo a passo)
Guia prático para migrar endpoints bloqueantes no Spring Boot para virtual threads (Java 21). Padrões, código funcional, tratamento de transações, segurança e…
Evitar exaustão de conexões JDBC ao usar Virtual Threads no Spring Boot
Evite exaustão do pool JDBC ao adotar virtual threads no Spring Boot: identifique o erro comum (criar virtual threads sem limites) e aplique padrões práticos…