E aí, dev que vive de ExecutorServicee CompletableFuture? Já tentou criar 10 mil threads na mão? O OutOfMemoryError vem cantando. Isso porque as threads clássicas do Java (agora chamadas de platform threads) são, basicamente, wrappers finos de threads do sistema operacional. Elas são caras – cada uma consome cerca de 20 MB de memória só de pilha (stack).
Mas a partir do JDK 21, temos as virtual threads – leves, baratas. Vamos entender sem mistério.
⚠️ Qual é o problema das Platform Threads (as de sempre)?
Uma thread de plataforma = uma thread do SO.
Isso é caro em tempo (criação lenta) e espaço (stack enorme: ~20 MB).
O escalonador do SO também tem trabalho para trocar o contexto entre elas.
Faça as contas: numa máquina com 8 GB de RAM, cabem no máximo umas 400 threads (8.000 MB / 20 MB = 400). Se essas threads ficarem a maior parte do tempo esperando I/O (como acesso a banco, chamadas HTTP), o CPU fica ocioso – você mal usa o hardware.
Exemplo: uma thread trabalha 0,001 ms preparando um request, depois espera 100 ms pela resposta da rede. Nesse cenário, ela fica parada 99,99% do tempo. Com 400 threads, o uso de CPU é menor que 1%. Um desperdício.
A solução tradicional? Usar pools de threads, CompletableFuture, programação reativa (Mono/Flux). Mas o código fica mais complexo, cheio de callbacks.
✨ Virtual Threads – a revolução
Virtual threads são leves – cada uma ocupa poucos bytes (não megabytes) e vivem no heap da JVM, não na pilha do SO. Criar uma virtual thread é ~1000 vezes mais barato que uma platform thread. Elas são gerenciadas pelo próprio JVM, não pelo escalonador do SO.
A arquitetura é de muitos para poucos: milhões de virtual threads rodam em cima de um pequeno número de platform threads (as velhas conhecidas). Cada platform thread executa uma virtual thread por vez, e quando a virtual thread bloqueia (ex: aguardando I/O), a JVM desmonta ela dali e a platform thread pega outra virtual thread pronta. Isso chama-se escalonamento cooperativo – ninguém fica parado.
📌 Nota importante:
Virtual threads não devem ser pooladas nem reutilizadas. São descartáveis, baratas, criamos uma para cada tarefa concorrente e pronto.
🛠️ Criando e usando virtual threads na prática
Vamos pegar uma tarefa simples:
Runnable task = () -> System.out.println(
Thread.currentThread().toString());
1. Criar e já iniciar com startVirtualThread()
Thread vThread = Thread.startVirtualThread(task);
vThread.setName("minhaVirtual");
2. Usando Thread.ofVirtual()
Thread vThread = Thread.ofVirtual().start(task);
// Com nome
Thread vThread = Thread.ofVirtual().name("minhaVirtual").start(task);
3. Usando Thread.Builder (útil para várias)
Thread.Builder builder = Thread.ofVirtual().name("vt-", 1);
Thread t1 = builder.start(task);
t1.join();
Thread t2 = builder.start(task);
t2.join();
4. Criar sem iniciar (unstarted)
Thread vThread = Thread.ofVirtual().unstarted(task);
// depois...
vThread.start();
5. ThreadFactory de virtual threads
ThreadFactory factory = Thread.ofVirtual().name("worker-").factory();
Thread vThread = factory.newThread(task);
vThread.start();
Fácil, né? Você usa a mesma API java.lang.Thread que já conhece. Não precisa aprender CompletableFuture ou reactive streams – o código fica sequencial e legível.
🔍 Detalhes que você precisa saber
-
isVirtual()→ retornatrue. - É daemon por padrão (
isDaemon()=true). Não tente mudar. - Prioridade sempre
NORM_PRIORITY(5).setPriority()não tem efeito. - Não pertence a
ThreadGrouptradicional – o grupo se chama "VirtualThreads". -
toString()mostra algo como:VirtualThread[#22]/runnable@ForkJoinPool-1-worker-1. OForkJoinPool-1-worker-1é a carrier thread (platform thread) que está executando sua virtual thread.
🚀 Quantas virtual threads conseguimos criar?
Rode isso (com cuidado, porque ele trava o sistema só depois de milhões):
AtomicLong contador = new AtomicLong();
while (true) {
Thread.startVirtualThread(() -> {
long id = contador.incrementAndGet();
System.out.println("Virtual thread: " + id);
LockSupport.park(); // trava a thread virtual
});
}
No meu notebook, depois de 14 milhões de virtual threads o sistema ficou lento (GC atuando forte), mas não estourou memória. Isso é outro patamar de concorrência.
❗ Mitos que você deve esquecer
- "Virtual threads são mais rápidas" – ERRADO. Elas não tornam código computacional mais rápido. Para CPU pesado, use parallel streams. Virtual threads melhoram throughput (você pode ter milhões esperando I/O), mas não latência.
- "Devemos poolá-las" – ERRADO. Nunca crie um pool de virtual threads. Elas são descartáveis, baratas. Cada tarefa ganha a sua.
- "Virtual threads são gratuitas" – ERRADO. Não são de graça, mas são 1000x mais baratas que platform threads.
- "Se bloquear a virtual thread, a carrier thread também trava" – ERRADO. A JVM desmonta a virtual thread bloqueada e a carrier thread pega outra. O bloqueio não afeta outras virtual threads.
🔄 Compatibilidade retrógrada
Virtual threads funcionam com:
- Blocos
synchronized -
ThreadLocal(mas cuidado com o número excessivo – pode não escalar) Thread.currentThread()-
interrupt()eInterruptedException
Ou seja, seu código antigo simplesmente roda em virtual threads – sem modificação. Basta mudar de new Thread(...).start() para Thread.startVirtualThread(...).
📌 Quando usar (e quando não usar)
✅ Ótimo para:
- Muitas tarefas de I/O (chamadas HTTP, leitura de banco, acesso a arquivos)
- Servidores web com alta concorrência
- Substituir
CompletableFuturee código reativo cheio de callbacks
❌ Não tão útil para:
- Processamento intensivo de CPU (cálculos, criptografia, compressão) – prefira parallel streams ou manter pool de platform threads com tamanho = número de cores
🧠 Exemplo prático: simulando 10.000 chamadas HTTP
Com virtual threads fica limpo:
var httpClient = HttpClient.newHttpClient();
var requests = IntStream.range(0, 10_000)
.mapToObj(i -> HttpRequest.newBuilder(URI.create("https://api.exemplo.com/users/" + i)).build())
.toList();
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
var futures = requests.stream()
.map(req -> executor.submit(() -> httpClient.send(req, BodyHandlers.ofString())))
.toList();
for (var future : futures) {
var response = future.get();
System.out.println(response.body().length());
}
}
Criamos 10.000 virtual threads ao mesmo tempo, cada uma esperando I/O. Com platform threads, isso seria impossível sem um pool enorme e OutOfMemoryError. Com virtual threads, roda liso.
🎯 Conclusão
Virtual threads são um divisor de águas para concorrência em Java:
- ✔️ Crie milhões delas sem medo
- ✔️ Código simples e bloqueante (nada de callbacks)
- ✔️ Aproveite ao máximo o hardware em tarefas de I/O
- ✔️ Compatível com todo o ecossistema Java
Então da próxima vez que pensar em CompletableFuture ou reactive streams, pergunte-se: dá pra resolver com virtual threads? Muitas vezes, a resposta é sim – com muito menos neuras.
Gostou? Testa aí no seu código com JDK 21+ e me conta o que achou. E lembre-se: thread virtual não é mais rápida, mas você pode ter tantas que parece mágica. ✨
Java21 #VirtualThreads #Concorrência #ProgramaçãoAssíncrona #JDK
Quer mais? O próximo post vai mostrar como usar virtual threads com Spring Boot e comparar performance com reactive. Até lá!
Top comments (0)