1. Escalabilidade
Por que threads convencionais não escalam bem?
Cada thread clássica (Thread) — é uma entidade do sistema operacional com sua própria pilha (geralmente 1–2 MB) e estrutura de estado. A tentativa de criar, digamos, 10 000 threads convencionais frequentemente leva ao erro OutOfMemoryError. Por isso, servidores tradicionais usam pools limitados de threads.
Threads virtuais: a magia da escalabilidade
Threads virtuais (Java 21+) — “leves”, gerenciadas pela JVM. Sua pilha fica no heap e pode crescer/encolher dinamicamente. Quando a thread bloqueia em I/O, a JVM a “congela” e continua executando outras tarefas.
Dentro da JVM há um pequeno pool de “portadores” (carrier threads) — threads de plataforma do SO nas quais os threads virtuais são executados em sequência. Isso permite criar 100 000+ tarefas sem problemas de memória. A JVM própria agenda quais threads virtuais executar e quando.
Demonstração: 100_000 threads virtuais contra 1_000 de plataforma
Exemplo: criação de 1000 threads convencionais
// Tentativa de criar 1000 threads convencionais
List<Thread> threads = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
Thread t = new Thread(() -> {
try {
Thread.sleep(10_000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
threads.add(t);
t.start();
}
System.out.println("Threads criados: " + threads.size());
Resultado: Na maioria dos sistemas é possível criar 1 000–2 000 threads; com valores maiores começam problemas de memória e lentidão.
Exemplo: criação de 100 000 threads virtuais
// Criamos 100_000 threads virtuais
List<Thread> vThreads = new ArrayList<>();
for (int i = 0; i < 100_000; i++) {
Thread t = Thread.ofVirtual().start(() -> {
try {
Thread.sleep(10_000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
vThreads.add(t);
}
System.out.println("Threads virtuais criados: " + vThreads.size());
Resultado: O programa cria tranquilamente 100 000 threads virtuais sem quedas e sem travamentos significativos. A memória exigida é muito menor.
Comparação visual
| Tipo de thread | Máximo de threads (aproximado) | Uso de memória | Tempo de inicialização |
|---|---|---|---|
| Convencionais (Thread) | 1 000 – 10 000 | Alto | Demorado |
| Virtuais | 100 000 – 1 000 000+ | Baixo | Instantâneo |
Fato: Threads virtuais permitem escrever código “uma thread por tarefa” sem pools complexos e sem risco de sobrecarregar o sistema.
2. Desempenho: onde os threads virtuais mostram seu potencial
Tarefas limitadas por I/O (I/O-bound)
Threads virtuais são excelentes para requisições de rede, I/O de arquivos e trabalho com bancos de dados. Quando a operação bloqueia, a thread virtual libera o “portador” e a JVM executa outras tarefas. Isso aumenta a vazão quando há grande número de esperas simultâneas.
Exemplo: simulação de 10 000 requisições HTTP simultâneas
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
HttpClient client = HttpClient.newHttpClient();
List<Thread> threads = new ArrayList<>();
for (int i = 0; i < 10_000; i++) {
Thread t = Thread.ofVirtual().start(() -> {
try {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://example.com"))
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println("Resposta: " + response.statusCode());
} catch (Exception e) {
System.out.println("Erro: " + e.getMessage());
}
});
threads.add(t);
}
// Aguardamos a conclusão de todas as threads
for (Thread t : threads) {
t.join();
}
Resultado: Todas as 10 000 requisições são executadas em paralelo; o programa não cai e o código permanece simples.
CPU-bound: threads virtuais não aceleram cálculos
Se a tarefa carrega a CPU, threads virtuais não trazem velocidade: a quantidade de núcleos é fixa. Aqui fazem sentido pools fixos, iguais ao número de núcleos, para não criar competição desnecessária.
// Cada tarefa calcula a soma de um grande intervalo
Runnable cpuTask = () -> {
long sum = 0;
for (int i = 0; i < 100_000_000; i++) {
sum += i;
}
System.out.println("Soma: " + sum);
};
// Executamos 1000 threads virtuais com cálculos
for (int i = 0; i < 1000; i++) {
Thread.ofVirtual().start(cpuTask);
}
Resultado: As threads vão competir por CPU, mas não haverá aceleração — não é uma tarefa para Virtual Threads.
3. Restrições e particularidades
Sincronização e armadilhas de threads virtuais
- Cuidado com bloqueios nativos. O uso de synchronized pode “colar” a thread virtual ao portador, reduzindo o ganho. Prefira ReentrantLock, Semaphore e outros primitivos de java.util.concurrent, otimizados para threads virtuais.
- Bibliotecas antigas. Alguns drivers JDBC e bibliotecas nativas ainda não estão otimizados para Virtual Threads. Teste cuidadosamente operações bloqueantes.
Não para tarefas de longa duração
Virtual Threads são ideais para unidades de trabalho “curtas”: processar uma requisição, uma operação e finalizar. Um milhão de tarefas que vivem para sempre (por exemplo, cálculos infinitos) não trarão benefício — para elas use threads de plataforma.
4. Boas práticas: onde usar threads virtuais
- Tarefas I/O-bound: chamadas de rede, arquivos, banco de dados — tudo em que a thread passa muito tempo esperando.
- Servidores web: processe cada requisição HTTP em uma thread virtual separada.
- Testes de integração: simule rapidamente milhares de clientes.
- Processamento assíncrono: escreva o código “bloqueante” de sempre — a JVM faz o agendamento inteligente.
Evite usar:
- Para tarefas que carregam a CPU o tempo todo.
- Quando a compatibilidade com bibliotecas de baixo nível é crítica (nem todas estão adaptadas ainda).
Por baixo dos panos, é conveniente usar o executor: Executors.newVirtualThreadPerTaskExecutor() — “uma thread virtual por tarefa”, sem pool fixo.
5. Monitoramento e medição: como ver threads virtuais em ação
JVisualVM e Flight Recorder
JVisualVM mostra threads ativas, seus estados e memória; a partir do Java 21, threads virtuais são exibidas separadamente. O Java Flight Recorder (JFR) grava uma “caixa-preta” detalhada da execução, incluindo estatísticas de Virtual Threads — útil para encontrar gargalos.
Como ver a quantidade de threads no código
Uma forma simples de ver a quantidade de threads na JVM:
System.out.println("Total de threads: " + Thread.activeCount());
Para contar quantas delas são virtuais:
long vCount = Thread.getAllStackTraces().keySet().stream()
.filter(Thread::isVirtual)
.count();
System.out.println("Threads virtuais: " + vCount);
6. Erros comuns ao trabalhar com threads virtuais
Erro nº 1: Uso de threads virtuais para cálculos pesados. Executar milhões de threads virtuais com tarefas CPU-bound não acelera o processador. Virtual Threads não são um “turbo” para cálculos.
Erro nº 2: Copiar padrões antigos às cegas. Não crie pools fixos de threads virtuais. Use Executors.newVirtualThreadPerTaskExecutor() e deixe a JVM escalar automaticamente.
Erro nº 3: Uso de bibliotecas não suportadas. Bloqueios nativos e bibliotecas não adaptadas ao Loom podem causar travamentos e quedas de desempenho. Verifique a compatibilidade com antecedência.
Erro nº 4: Otimização prematura. Se você tem poucas threads e uma multithread convencional, não se apresse em migrar tudo para Virtual Threads. A ferramenta é boa onde há I/O massivo e espera.
Erro nº 5: Ignorar o monitoramento. Criar um milhão de tarefas é fácil, mas sem monitoramento e tratamento de exceções você pode obter um “benchmark bonito” em vez de um sistema confiável. Use JVisualVM e JFR.
GO TO FULL VERSION