CodeGym /Cursos /JAVA 25 SELF /Escalabilidade e desempenho de Virtual Threads

Escalabilidade e desempenho de Virtual Threads

JAVA 25 SELF
Nível 57 , Lição 2
Disponível

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 12 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 0002 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 00010 000 Alto Demorado
Virtuais 100 0001 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.

Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION