CodeGym /Cursos /JAVA 25 SELF /Uso de Executor com threads virtuais

Uso de Executor com threads virtuais

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

1. Em poucas palavras

Você já está um pouco familiarizado com as classes do pacote java.util.concurrent, especialmente com o ExecutorService. Ele é um “gerenciador de tarefas”: você envia um trabalho para ele (por exemplo, via submit()), e ele mesmo decide quando e em qual thread executá-lo. Normalmente, por baixo dos panos, funciona um pool de threads de tamanho fixo, que economiza recursos e não cria uma nova thread para cada tarefa.

No entanto, com threads virtuais tudo muda! Agora dá para se dar ao luxo: uma thread por tarefa — e sem medo de a JVM estourar.

Nova forma: Executors.newVirtualThreadPerTaskExecutor()

No Java 21 surgiu uma nova forma de criar um ExecutorService que executa cada tarefa em uma thread virtual separada:

ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();

Diferença importante:

  • Os pools de threads antigos (Executors.newFixedThreadPool, Executors.newCachedThreadPool) limitavam a quantidade de tarefas simultâneas por causa do alto custo das threads do SO.
  • O novo Executor virtual é quase sem limites: uma thread virtual própria para cada tarefa.

Exemplo simples

Vamos enviar 10 tarefas para o Executor virtual:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class VirtualExecutorDemo {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();

        for (int i = 1; i <= 10; i++) {
            int taskId = i; // capturamos a variável para a lambda
            executor.submit(() -> {
                System.out.println("Task " + taskId + " is running in thread: " +
                        Thread.currentThread());
            });
        }

        executor.shutdown();
    }
}

O que acontece?
Cada tarefa será executada na sua própria thread virtual, e você verá linhas como:

Task 1 is running in thread: VirtualThread[#24]/runnable@ForkJoinPool-1-worker-1
...

2. Paralelismo em massa: milhares de tarefas — sem problema!

Para sentir toda a força das threads virtuais, vamos tentar enviar para o ExecutorService não 10, mas, digamos, 100_000 tarefas. Em pools clássicos, isso seria como tentar enfiar um elefante na geladeira: a JVM logo ficaria sem memória ou começaria a travar feio. Com threads virtuais — é diferente!

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class VirtualExecutorMassiveDemo {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();

        for (int i = 1; i <= 100_000; i++) {
            int taskId = i;
            executor.submit(() -> {
                // Para o exemplo — apenas dormimos 1 ms
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
                // Não imprimimos, caso contrário haverá linhas demais!
            });
        }

        executor.shutdown();
    }
}

Atenção: imprimir 100_000 linhas na tela é uma má ideia: o console vai “engasgar” mais rápido do que as threads virtuais. É melhor não escrever no console ou imprimir apenas as primeiras poucas tarefas.

3. Como funciona newVirtualThreadPerTaskExecutor

Resumindo: este ExecutorService cria uma nova thread virtual para cada tarefa que você envia a ele. Ao contrário de um pool fixo, aqui não há fila de tarefas nem limites rígidos para a quantidade de threads simultâneas (exceto os limites da sua JVM e do hardware).

Arquitetura:

  • As threads virtuais são mapeadas para um pequeno pool de threads reais (carrier) do SO.
  • A JVM decide quando e qual thread virtual iniciar, suspender e retomar.
  • Se a thread bloquear (por exemplo, na leitura de arquivo ou esperando rede), a JVM pode “congelar” a thread virtual e liberar a thread carrier para outras tarefas.

4. Exemplo: processamento de resultados com Future

O ExecutorService retorna um objeto do tipo Future quando a tarefa retorna um resultado. Tudo funciona da mesma forma que com threads “normais”:

import java.util.concurrent.*;

public class VirtualExecutorWithResult {
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();

        Future<String> future = executor.submit(() -> {
            Thread.sleep(500);
            return "Hello from virtual thread!";
        });

        System.out.println("Result: " + future.get()); // Aguardamos o resultado

        executor.shutdown();
    }
}

Tudo como de costume: você pode enviar tarefas que retornam valor, esperar o resultado via get(), e as exceções são tratadas de forma padrão.

5. Como encerrar corretamente o Executor

É muito importante não esquecer de encerrar o ExecutorService para que o programa não congele (mesmo que as threads sejam virtuais, e não “reais”).

shutdown() e awaitTermination

executor.shutdown(); // Dizemos: não aceitamos mais tarefas
executor.awaitTermination(1, TimeUnit.MINUTES); // Aguardamos a conclusão de todas as tarefas (no máximo 1 minuto)

Por que isso é importante?
Se não chamar shutdown(), as threads virtuais podem continuar vivas, e o programa não vai terminar mesmo após a execução de main(). Esse é um erro típico de iniciantes.

6. Nuances úteis

Comparação: Executor virtual vs. pool de threads clássico

Pool clássico (newFixedThreadPool) Executor virtual (newVirtualThreadPerTaskExecutor)
Quantidade de threads Limitada pelo tamanho do pool Uma thread virtual por tarefa, quase sem limite
Tarefas na fila Sim, se todas as threads estiverem ocupadas Em geral, não: a tarefa recebe uma thread imediatamente
Custo da thread Alto (stack, recursos do SO) Muito baixo (agendamento pela JVM)
Escalabilidade Limitada Quase ilimitada
Para que é indicado Tarefas CPU-bound, paralelismo limitado Tarefas I/O-bound, paralelismo massivo

Integração com servidores web

Servidores web modernos (por exemplo, Tomcat, Jetty, Undertow) já começam a oferecer suporte a threads virtuais. Isso significa que é possível processar cada requisição HTTP em uma thread virtual separada, sem medo de “engasgar” com a enxurrada de usuários.

Vantagem: não é preciso inventar esquemas assíncronos complexos com callbacks e CompletableFuture; o código fica mais simples — você pode escrever o código bloqueante de sempre, mas o aplicativo ainda assim escala.

Testes em massa e simulação de carga

As threads virtuais são ótimas para testes em que é preciso “simular” milhares de usuários, requisições ou operações simultâneas. Por exemplo, um teste que envia 10_000 requisições paralelas para um servidor, cada uma em sua própria thread virtual.

Processamento paralelo de arquivos e conexões de rede

Se o aplicativo trabalha com muitos arquivos ou conexões de rede, você pode processar cada conexão em uma thread virtual separada, sem se preocupar com o gerenciamento manual de pools.

7. Erros comuns ao trabalhar com Executors virtuais

Erro nº 1: esqueceu de chamar shutdown(). Se não encerrar o Executor, o programa não vai terminar — as threads virtuais ainda estarão aguardando novas tarefas. Se necessário, adicione awaitTermination(...).

Erro nº 2: usar threads virtuais para cálculos pesados. As threads virtuais não aceleram tarefas que consomem 100% de CPU. Para CPU-bound, é melhor usar um pool fixo (Executors.newFixedThreadPool) e ajustar cuidadosamente o tamanho.

Erro nº 3: ignorar exceções dentro das tarefas. Se a tarefa lançar uma exceção, ela não chegará à thread principal — trate via Future (método get()) ou com try/catch dentro da lambda.

Erro nº 4: confundir a sintaxe/versão antiga com a nova do JDK. Verifique se você está usando a versão correta do JDK (Java 21+) e se a IDE está configurada para dar suporte a threads virtuais. O método específico é Executors.newVirtualThreadPerTaskExecutor().

Erro nº 5: depender de ThreadLocal para transportar contexto. As threads virtuais frequentemente são criadas e destruídas; o ThreadLocal pode não se comportar como esperado. Para transporte de contexto, use ScopedValue (Scoped Values; mais detalhes — na próxima aula).

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