DEV Community

Cover image for Virtual Threads – adeus, `OutOfMemoryError`; olá, milhões de threads
Fabio Rocha
Fabio Rocha

Posted on

Virtual Threads – adeus, `OutOfMemoryError`; olá, milhões de threads

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());
Enter fullscreen mode Exit fullscreen mode

1. Criar e já iniciar com startVirtualThread()

Thread vThread = Thread.startVirtualThread(task);
vThread.setName("minhaVirtual");
Enter fullscreen mode Exit fullscreen mode

2. Usando Thread.ofVirtual()

Thread vThread = Thread.ofVirtual().start(task);
// Com nome
Thread vThread = Thread.ofVirtual().name("minhaVirtual").start(task);
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

4. Criar sem iniciar (unstarted)

Thread vThread = Thread.ofVirtual().unstarted(task);
// depois...
vThread.start();
Enter fullscreen mode Exit fullscreen mode

5. ThreadFactory de virtual threads

ThreadFactory factory = Thread.ofVirtual().name("worker-").factory();
Thread vThread = factory.newThread(task);
vThread.start();
Enter fullscreen mode Exit fullscreen mode

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() → retorna true.
  • É daemon por padrão (isDaemon() = true). Não tente mudar.
  • Prioridade sempre NORM_PRIORITY (5). setPriority() não tem efeito.
  • Não pertence a ThreadGroup tradicional – o grupo se chama "VirtualThreads".
  • toString() mostra algo como: VirtualThread[#22]/runnable@ForkJoinPool-1-worker-1. O ForkJoinPool-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
    });
}
Enter fullscreen mode Exit fullscreen mode

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() e InterruptedException

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 CompletableFuture e 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());
    }
}
Enter fullscreen mode Exit fullscreen mode

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)