Arquitetura e Funcionamento de Thread Pools em Java

Conceito e Benefícios

Em Java, a criação e destruição contínua de threads para cada requisição consome recursos significativos do sistema operacional e da JVM. O excesso de threads pode levar a problemas de memória e sobrecarga de troca de contexto (context switching). O pool de threads resolve isso mantendo um conjunto de threads pré-inicializadas em um contêiner. As tarefas são atribuídas a essas threads disponíveis, que são reutilizadas após a conclusão, minimizando a sobrecarga de criação e otimizando o uso de recursos.

Benefícios principais:

  • Redução drástica do overhead de criação e destruição de threads.
  • Melhoria na latência de resposta, pois as threads já estão prontas para uso.
  • Controle rigoroso sobre o consumo de recursos, prevenindo a exaustão do sistema.

O Núcleo ThreadPoolExecutor

As fábricas padrão da classe Executors são construídas sobre o ThreadPoolExecutor. Compreender seus parâmetros é essencial para configurações avançadas:

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler)
  • corePoolSize: Quantidade de threads que permanecem ativas mesmo quando ociosas.
  • maximumPoolSize: Limite máximo de threads que o pool pode criar.
  • keepAliveTime e unit: Tempo de sobrevivência de threads excedentes ao núcleo quando ociosas.
  • workQueue: Fila para armazenar tarefas aguardando execução.
  • threadFactory: Fábrica para gerar novas threads.
  • handler: Política de rejeição quando o pool e a fila estão saturados.

Fábricas da Classe Executors

Embora o uso direto do ThreadPoolExecutor seja recomendado para evitar riscos de OOM (Out Of Memory), as fábricas do Executors oferecem atalhos:

FixedThreadPool

Cria um pool com um número fixo de threads. Utiliza uma LinkedBlockingQueue sem limite prático.

public static ExecutorService criarPoolFixo(int totalThreads) {
    return new ThreadPoolExecutor(totalThreads, totalThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<>());
}

Fluxo: Se threads ativas < corePoolSize, cria nova thread. Caso contrário, enfileira a tarefa. Threads ociosas não são encerradas. Ideal para cargas de trabalho estáveis.

CachedThreadPool

Pool dinâmico sem threads centrais (corePoolSize = 0) e limite máximo de Integer.MAX_VALUE. Usa SynchronousQueue.

public static ExecutorService criarPoolCacheado() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<>());
}

Fluxo: Tarefas são delegadas diretamente. Se não houver thread livre, uma nova é criada. Threads ociosas por 60 segundos são destruídas. Adequado para muitas tarefas curtas e assíncronas.

SingleThreadExecutor

Garante a execução sequencial de tarefas usando apenas uma thread de trabalho, com uma fila desvinculada para garantir que nenhuma tarefa seja perdida em caso de falha.

Filas de Trabalho e Políticas de Rejeição

A workQueue armazena tarefas pendentes. Tipos comuns:

  • ArrayBlockingQueue: Fila baseada em array com capacidade fixa.
  • LinkedBlockingQueue: Fila baseada em lista encadeada, capacidade opcional (padrão Integer.MAX_VALUE).
  • SynchronousQueue: Não armazena elementos; cada inserção deve aguardar uma remoção correspondente.

Quando o pool e a fila estão cheios, o RejectedExecutionHandler é acionado:

  1. AbortPolicy: Lança RejectedExecutionException (padrão).
  2. CallerRunsPolicy: A thread que chamou o método de execução processa a tarefa.
  3. DiscardOldestPolicy: Descarta a tarefa mais antiga na fila e tenta novamente.
  4. DiscardPolicy: Descatra a nova tarefa silenciosamente.

Dimensionamento e Ciclo de Vida

Configurar o tamanho ideal depende da natureza da tarefa:

  • CPU Intensivo: Foco em processamento. O número de threads deve ser próximo ao de núcleos lógicos da CPU (ex: Nucleos + 1).
  • I/O Intensivo: Foco em operações de rede ou disco. As threads passam muito tempo bloqueadas. Pode-se usar mais threads (ex: Nucleos * 2 ou a fórmula: Nucleos * (1 + TempoEspera / TempoComputacao)).

Inciialização e Encerramento

Por padrão, threads são criadas sob demanda. Para pré-inicializar:

ThreadPoolExecutor executor = (ThreadPoolExecutor) servico;
executor.prestartAllCoreThreads();

Para encerrar:

  • shutdown(): Impede novas tarefas e aguarda as atuais terminarem.
  • shutdownNow(): Interrompe tarefas em execução e retorna as pendentes.

Ajustes dinâmicos podem ser feitos via setCorePoolSize() e setMaximumPoolSize().

Monitoramento via Hooks

O ThreadPoolExecutor permite interceptar a execução de tarefas sobrescrevendo métodos de hook. Exemplo de monitoramento de tempo de execução:

public class PoolMonitorizado extends ThreadPoolExecutor {
    private final Map<String, Long> temposInicio = new ConcurrentHashMap<>();

    public PoolMonitorizado(int core, int max, long keepAlive, TimeUnit unit, BlockingQueue<Runnable> queue) {
        super(core, max, keepAlive, unit, queue);
    }

    @Override
    protected void beforeExecute(Thread t, Runnable r) {
        temposInicio.put(String.valueOf(r.hashCode()), System.currentTimeMillis());
        super.beforeExecute(t, r);
    }

    @Override
    protected void afterExecute(Runnable r, Throwable t) {
        Long inicio = temposInicio.remove(String.valueOf(r.hashCode()));
        if (inicio != null) {
            long duracao = System.currentTimeMillis() - inicio;
            System.out.println("Tarefa concluída em " + duracao + "ms. Threads ativas: " + getActiveCount());
        }
        super.afterExecute(r, t);
    }
    
    public static ExecutorService criarMonitorado() {
        return new PoolMonitorizado(2, 4, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(100));
    }
}

Execução Assíncrona com Callable e Future

O pool aceita tarefas via execute() ou submit().

  • execute(Runnable): Não retorna resultado e exceções não tratadas podem encerrar a thread.
  • submit(Runnable/Callable): Retorna um Future, permitindo recuperar o resultado ou verificar o status da tarefa. Exceções são encapsuladas no Future e lançadas apenas ao chamar get().

Exemplo com Callable:

public class TarefaAssincrona implements Callable<String> {
    @Override
    public String call() throws Exception {
        Thread.sleep(500);
        return "Resultado processado";
    }

    public static void main(String[] args) throws Exception {
        ExecutorService pool = Executors.newSingleThreadExecutor();
        Future<String> futuro = pool.submit(new TarefaAssincrona());
        
        System.out.println("Aguardando conclusão...");
        String resultado = futuro.get();
        System.out.println(resultado);
        
        pool.shutdown();
    }
}

O uso de Future e Callable é fundamental para operações não bloqueantes, permitindo que a thread principal continue sua execução ou processe outros dados enquanto a tarefa demorada é computada em segundo plano.

Tags: java ThreadPool ThreadPoolExecutor Concorrência Callable

Publicado em 7-23 00:37