Virtual Threads não são mágica: Concorrência vs Paralelismo no Java 21

TL;DR: Virtual Threads são uma ferramenta de Concorrência, não de Paralelismo. Entender essa distinção é o primeiro passo para projetar sistemas que não apenas rodam, mas escalam de verdade.
Introdução
Nos últimos posts falamos sobre Virtual Threads, Pinning e as demais novidades do Java 21. Mas existe um conceito fundamental que precisa estar na ponta da língua antes de qualquer decisão arquitetural: a real diferença entre Concorrência e Paralelismo.
Esses termos são frequentemente usados como sinônimos e esse é exatamente o erro que faz desenvolvedores usarem Virtual Threads da forma errada, sem obter os ganhos esperados.
O que é Paralelismo?
Paralelismo é sobre latência. O objetivo é diminuir o tempo de execução de uma única unidade de trabalho, dividindo-a em partes menores que rodam simultaneamente em múltiplos núcleos de CPU.
Tarefa complexa
│
├──▶ Core 1: subtarefa A
├──▶ Core 2: subtarefa B
├──▶ Core 3: subtarefa C
└──▶ Core 4: subtarefa D
No paralelismo, as threads são parte da solução/algoritmo. Você deliberadamente decompõe o problema para explorar múltiplos núcleos ao mesmo tempo.
Exemplo clássico no Java:
// Paralelismo: processamento de uma lista grande em múltiplos cores
List<Pedido> pedidos = buscarPedidos();
double totalFaturado = pedidos.parallelStream()
.filter(Pedido::isPago)
.mapToDouble(Pedido::getValor)
.sum();
// ✓ Divide o trabalho entre os cores disponíveis
Quando usar:
- Processamento de grandes volumes de dados (ETL, relatórios)
- Cálculos matemáticos intensivos
- Renderização de imagens ou vídeos
- Machine learning e inferência local
O que é Concorrência?
Concorrência é sobre throughput (vazão). O objetivo é completar o maior volume de tarefas possível em um intervalo de tempo, fazendo com que múltiplas tarefas progridam de forma intercalada (time-slicing), compartilhando os mesmos recursos.
Req #1 ──▶ [CPU] ──▶ [I/O wait...] ──▶ [CPU] ──▶ ✓
Req #2 ──▶ [CPU] ──▶ [I/O wait...] ──▶ ✓
Req #3 ──▶ [CPU] ──▶ [I/O wait...] ──▶ ✓
^ ^
| |
mesmo carrier mesmo carrier
Na concorrência, as tarefas são parte do problema a ser resolvido — por exemplo, lidar com milhares de usuários fazendo requisições simultaneamente.
Exemplo clássico no Java:
// Concorrência: lidar com milhares de requisições HTTP
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (Requisicao req : requisicoes) {
executor.submit(() -> processarRequisicao(req));
}
}
// ✓ Cada requisição em sua própria Virtual Thread
// ✓ I/O libera o carrier, que processa outra tarefa
Quando usar:
- Servidores HTTP com alto volume de requisições
- Aplicações que fazem muitas chamadas a banco de dados
- Integrações com APIs externas (I/O intensivo)
- Sistemas de mensageria e filas
Comparativo direto
| Critério | Paralelismo | Concorrência |
|---|---|---|
| Foco | Latência (velocidade) | Throughput (volume) |
| Problema | 1 tarefa, N núcleos | N tarefas, 1 fluxo |
| Threads | Parte da solução | Parte do problema |
| Gargalo | CPU-bound | I/O-bound |
| Ferramenta Java | ForkJoinPool, parallelStream() |
Virtual Threads |
| Métrica de sucesso | Tempo de execução ↓ | Requisições por segundo ↑ |
Por que Virtual Threads são Concorrência — não Paralelismo
Esse é o ponto central. Virtual Threads não foram criadas para:
❌ Fazer um cálculo individual rodar mais rápido
❌ Substituir parallelStream() em processamento de dados
❌ Reduzir a latência de uma operação CPU-bound
Elas foram criadas para:
✅ Suportar throughput massivo em operações I/O-bound
✅ Eliminar o custo de manter threads de OS ociosas durante a espera
✅ Permitir que um servidor lide com milhões de conexões simultâneas
O segredo: otimizar a espera
O Project Loom não veio para acelerar o código. Veio para otimizar o tempo que o sistema passa esperando — esperando resposta do banco de dados, da API externa, do sistema de arquivos.
Thread de OS tradicional:
[CPU] ────▶ [ I/O wait (bloqueado) ] ────▶ [CPU]
↑ OS thread desperdiçada aqui
Virtual Thread com Project Loom:
[CPU] ──▶ [unmount] ──▶ [CPU de outra VT] ──▶ [mount] ──▶ [CPU]
↑ carrier liberado para processar outra tarefa!
Em uma thread de OS tradicional, o sistema operacional mantém a thread inteira bloqueada durante o I/O. Com Virtual Threads, a JVM desmonta (unmount) a thread virtual do carrier thread durante a espera, liberando esse recurso para processar outra Virtual Thread.
Armadilhas comuns
❌ Usar Virtual Threads para CPU-bound
// ERRADO: Virtual Threads não ajudam aqui
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
executor.submit(() -> calcularFibonacci(50)); // CPU-bound puro
}
// CORRETO: Use ForkJoinPool para CPU-bound
ForkJoinPool pool = ForkJoinPool.commonPool();
pool.submit(() -> calcularFibonacci(50));
❌ Criar pool de Virtual Threads
// ERRADO: Virtual Threads são descartáveis — não faça pool!
ExecutorService pool = Executors.newFixedThreadPool(
100, Thread.ofVirtual().factory()
);
// CORRETO: Uma VT por tarefa, deixe a JVM gerenciar
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
❌ Ignorar o Pinning com synchronized
// ERRADO: synchronized impede o unmounting da VT
synchronized (lock) {
Thread.sleep(1000); // VT fica presa — sem unmounting!
}
// CORRETO: ReentrantLock permite unmounting
private final ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
Thread.sleep(1000); // VT é desmontada, carrier liberado ✓
} finally {
lock.unlock();
}
Como monitorar e diagnosticar
Detectar Pinning
# Rastrear threads presas (stack trace resumido)
-Djdk.tracePinnedThreads=short
# Stack trace completo para análise detalhada
-Djdk.tracePinnedThreads=full
Verificar throughput com JFR
# Gravar Flight Recording para análise de concorrência
java -XX:StartFlightRecording=filename=recording.jfr,duration=60s \
-jar sua-aplicacao.jar
Métricas no Spring Boot (Micrometer)
// Monitorar tarefas concorrentes em execução
@Bean
MeterBinder virtualThreadMetrics() {
return registry -> Gauge.builder("vt.active",
executor, e -> ((ThreadPoolExecutor) e).getActiveCount())
.register(registry);
}
Guia de decisão rápida
Minha operação é...
│
├──▶ CPU intensiva (cálculo, transformação de dados)?
│ └──▶ Use ForkJoinPool / parallelStream() [PARALELISMO]
│
└──▶ I/O intensiva (banco, API, fila, arquivo)?
└──▶ Use Virtual Threads [CONCORRÊNCIA]
Conclusão
O Java 21 entregou uma das maiores mudanças na história da plataforma com o Project Loom — mas para aproveitá-la de verdade, precisamos entender para quê ela foi desenhada.
Virtual Threads são a resposta do Java para o problema de escalar sistemas I/O-bound sem esgotar os recursos do sistema operacional. Elas não tornam seu código mais rápido, elas tornam seu sistema capaz de atender muito mais usuários com os mesmos recursos.
Se você está lidando com processamento pesado de CPU, o caminho continua sendo o paralelismo com ForkJoinPool. Se você está construindo APIs, serviços de integração ou qualquer sistema com alto I/O, as Virtual Threads são o caminho.
A distinção parece sutil. O impacto na arquitetura, não.
Referências
- JEP 444: Virtual Threads (Java 21)
- JEP 453: Structured Concurrency (Java 21)
- Inside Java Podcast — Project Loom
- Java Concurrency in Practice — Brian Goetz
Publicado por: Guilherme Gomes - 21/05/2026 08:00
Caramelo.dev