Guias / java-threads
Java Threads
Platform threads, virtual threads e o modelo de concorrência da JVM: como funcionam, quando usar e as armadilhas.
24 abr 2026· 9 min· 10 seções
Neste guia · 10 seções
Threads ou virtual threads?
Uma thread é uma linha de execução: um trecho de código rodando com sua própria pilha de chamadas. Ter várias threads é a forma clássica de o Java lidar com várias coisas ao mesmo tempo, o que é concorrência, não necessariamente paralelismo.
Desde o Java 21 existem dois tipos: a platform thread, a thread de sempre, e a virtual thread. Os dois usam a mesma classe Thread e a mesma API; a diferença está em quem as gerencia e quanto custam.
O modelo tradicional: platform threads
- Mapeamento 1:1: cada
Threaddo Java corresponde a uma thread do sistema operacional, e é o SO quem decide quando ela roda. - Custo elevado: cada uma reserva uma pilha fixa (1 MB por padrão no Linux x64, ajustável com
-Xss), e criar ou trocar de thread (context switch) passa pelo kernel. - Limite de escala: na prática, alguns milhares por processo. Por isso sempre usamos pools com um número fixo de threads.
ExecutorService pool = Executors.newFixedThreadPool(200);pool.submit(() -> handle(request)); // no máximo 200 tarefas ao mesmo tempoO problema do I/O bloqueante
No Java clássico, quando uma thread faz uma chamada de rede ou de banco, ela fica parada esperando a resposta, mas continua ocupando a thread do SO e a memória dela.
A Lei de Little mostra por que isso limita o servidor: requisições simultâneas = taxa de chegada × tempo de resposta. Se chegam 10.000 requisições por segundo e cada uma leva 0,5 s esperando o banco, são 5.000 requisições em andamento, ou seja, 5.000 threads ocupadas, quase todas só esperando. Com um pool de 200, o resto entra na fila; com 5.000 platform threads, a memória e o SO viram o gargalo.
A saída antiga era o código assíncrono (CompletableFuture, frameworks reativos): escala bem, mas é difícil de ler, de depurar e de seguir no stack trace.
Virtual threads (Java 21+)
Virtual threads são threads gerenciadas pela JVM, não pelo SO.
- Mapeamento M:N: milhares (ou milhões) de virtual threads rodam sobre poucas platform threads, chamadas carrier threads. Por padrão, uma por núcleo de CPU, num
ForkJoinPoolpróprio da JVM. - Leveza: a pilha de uma virtual thread é um objeto no heap que cresce e encolhe conforme o uso. Criar 1 milhão delas é viável.
- Desbloqueio: quando uma virtual thread bloqueia em I/O, a JVM a desmonta da carrier, que fica livre para outra virtual thread. Quando a resposta chega, ela é montada de novo, em qualquer carrier livre, e continua de onde parou.
O que acontece quando uma virtual thread espera o banco:
Criando virtual threads
// uma thread avulsaThread.startVirtualThread(() -> handle(request));Thread.ofVirtual().name("worker").start(() -> handle(request));
// uma virtual thread por tarefa (adaptado da JEP 444)try (var executor = Executors.newVirtualThreadPerTaskExecutor()) { IntStream.range(0, 10_000).forEach(i -> executor.submit(() -> { Thread.sleep(Duration.ofSeconds(1)); return i; }));} // o close() espera todas terminaremAs 10.000 tarefas terminam em pouco mais de 1 segundo. Com um pool fixo de 200 platform threads, levariam cerca de 50.
O que mudou na prática
Simplicidade: você volta a escrever código síncrono, fácil de ler e de depurar, com a escalabilidade que antes exigia código assíncrono.
Abandono do pool: antes limitávamos o número de threads para não derrubar o servidor. Virtual threads são baratas, então a recomendação é uma thread por tarefa, descartada no final. Não faça pool de virtual threads.
Limite o recurso, não a thread: se o banco aguenta 50 conexões, quem limita é um Semaphore (ou o pool de conexões), não o número de threads.
private static final Semaphore DB_PERMITS = new Semaphore(50);
String query(String sql) throws InterruptedException { DB_PERMITS.acquire(); try { return database.execute(sql); } finally { DB_PERMITS.release(); }}Nos frameworks
Quase ninguém cria threads na mão em uma aplicação web: quem decide em que thread cada requisição roda é o framework. Por isso, adotar virtual threads costuma ser uma configuração, e não uma reescrita.
| Spring Boot | Quarkus | Helidon 4 | |
|---|---|---|---|
| Como ativar | Uma propriedade, vale para a aplicação toda | Anotação por método (@RunOnVirtualThread) | Já é o padrão |
| Modelo padrão sem isso | Pool de threads do servidor | Event loop (reativo) ou pool de workers | Virtual threads |
| Versão | Boot 3.2+ e Java 21+ (Java 24+ recomendado) | Quarkus 3 e Java 21+ | Helidon 4 e Java 21+ |
Spring Boot
Uma propriedade troca para virtual threads o processamento de requisições do Tomcat e do Jetty, o executor de @Async, o agendador de @Scheduled e os listeners de mensageria (Kafka, RabbitMQ):
spring.threads.virtual.enabled=true# virtual threads são daemon: sem isto, uma app só com @Scheduled pode encerrar sozinhaspring.main.keep-alive=trueO controller continua igual. Dois cuidados: com virtual threads, as propriedades de tamanho de pool (spring.task.execution.pool.*, server.tomcat.threads.max) deixam de ter efeito, então os limites passam a ser do banco e das conexões; e a própria documentação recomenda Java 24+, por causa do pinning (veja abaixo).
Quarkus
O Quarkus nasceu reativo: por padrão, o código roda no event loop do Vert.x ou num pool de workers. Virtual threads são opt-in por endpoint, e os três modelos convivem na mesma aplicação:
@Path("/orders")public class OrderResource {
@GET @RunOnVirtualThread // este método roda numa virtual thread nova a cada chamada public List<Order> list() { return repository.listAll(); // bloqueante, e tudo bem }}A anotação também funciona em consumidores de mensageria, em tarefas do @Scheduled e em serviços gRPC. A documentação do Quarkus alerta para bibliotecas que guardam objetos em ThreadLocal como cache (cita Jackson e Netty): com uma thread nova por chamada, o cache é recriado toda vez e o consumo de memória sobe.
Helidon 4
O servidor web do Helidon 4 foi reescrito sobre virtual threads: cada requisição já roda na sua, sem configurar nada. É o exemplo de framework pensado desde o início para esse modelo.
Armadilhas
- Não são threads mais rápidas. Elas aumentam a vazão (mais tarefas ao mesmo tempo), não a velocidade de cada uma. Em trabalho de CPU (cálculo, compressão) não há ganho: use platform threads ou um pool do tamanho do número de núcleos.
- Pinning: em alguns casos a virtual thread não consegue se desmontar e “prende” a carrier enquanto espera.
- No Java 21 a 23, isso acontecia ao bloquear dentro de um bloco
synchronized. A recomendação era trocar porReentrantLock. - No Java 24+ (JEP 491),
synchronizednão prende mais. Continua prendendo quando o bloqueio acontece dentro de código nativo (JNI ou Foreign Function API) que chamou de volta o Java. - Para investigar: o evento
jdk.VirtualThreadPinneddo Java Flight Recorder.
- No Java 21 a 23, isso acontecia ao bloquear dentro de um bloco
ThreadLocalpesado: com milhões de threads, um cache grande emThreadLocalvira milhões de cópias. PrefiraScopedValue(abaixo) ou passe o valor explicitamente.
Além das virtual threads
Scoped Values (final no Java 25)
Um substituto mais seguro para ThreadLocal: o valor é imutável e só existe dentro do escopo em que foi definido.
static final ScopedValue<User> CURRENT_USER = ScopedValue.newInstance();
ScopedValue.where(CURRENT_USER, user).run(() -> handle(request));
// em qualquer ponto da cadeia de chamadas dentro de handle():User user = CURRENT_USER.get();Structured Concurrency (preview)
Trata um grupo de subtarefas como uma unidade: elas começam e terminam dentro de um bloco, e se uma falha, as outras são canceladas.
// JDK 27: java --enable-preview --source 27 ...Response handle() throws InterruptedException, ExecutionException { try (var scope = StructuredTaskScope.open()) { Subtask<User> user = scope.fork(() -> findUser()); Subtask<Order> order = scope.fork(() -> fetchOrder()); scope.join(); // espera as duas; se uma falhar, cancela a outra return new Response(user.get(), order.get()); }}A API ainda muda a cada versão: no Java 27 está na sétima preview (JEP 533), e tutoriais de versões anteriores podem não compilar.
Quando usar cada uma
| Situação | Use |
|---|---|
| Servidor com muitas requisições esperando I/O (HTTP, banco, filas) | Virtual threads, uma por tarefa |
| Trabalho pesado de CPU | Platform threads, pool do tamanho dos núcleos |
| Limitar acesso a um recurso escasso | Semaphore ou o pool do próprio recurso |
| Várias chamadas em paralelo dentro de uma requisição | Virtual threads + Structured Concurrency (quando sair do preview) |
| Passar contexto (usuário, trace id) pela chamada | ScopedValue |
Vídeos
- Project Loom and Virtual Threads: Next Phases (Alan Bateman, Oracle, Devoxx Belgium 2024): boas práticas, a correção do pinning e Structured Concurrency, por quem trabalha no projeto (é o vídeo embutido no topo).
Referências
- JEP 444: Virtual Threads: a especificação, com a motivação e as recomendações de uso.
- JEP 491: Synchronize Virtual Threads without Pinning: a correção do
synchronized. - JEP 533: Structured Concurrency (seventh preview): as mudanças do Java 27 (InfoQ).
- Java Structured Concurrency in JDK 27: um exemplo completo, com o repositório (Dan Vega).
- Spring Boot: virtual threads e task execution and scheduling: o que a propriedade muda e o aviso sobre
keep-alive. - Quarkus: virtual thread support: os três modelos de execução e as armadilhas.
- Helidon 4: what’s new: o servidor sobre virtual threads.