Dominando o ThreadPoolExecutor no Java: Parâmetros e Estratégias de Rejeição

Em aplicações de alto desempenho, a criação e destruição constante de threads pode se tornar um gargalo significativo. Cada thread no Java consome recursos de memória e exige ciclos de CPU para sua inicialização e finalização. Para resolver esse problema, o Java utiliza o conceito de Pool de Threads, que permite a reutilização de threads existentes para executar múltiplas tarefas ao longo do ciclo de vida da aplicação.

Vantagens da Utilização de Thread Pools

  • Economia de Recursos: Evita o overhead associado ao ciclo de vida das threads, permitindo que threads de trabalho processem diversas requisições sequencialmente.
  • Rapidez na Resposta: Como as threads já estão instanciadas, o tempo de latência para iniciar a execução de uma tarefa é drasticamente reduzido.
  • Controel de Concorrência: Permite limitar o número máximo de threads ativas, evitando que o sistema fique sobrecarregado e cause falhas por falta de memória (OutMemoryError).

Criação de Pools com a Clase Executors

A biblioteca java.util.concurrent fornece a factory Executors, que facilita a criação de pools pré-configurados para cenários comuns:

Método Descrição
newFixedThreadPool(int n) Cria um pool com um número fixo de threads. Ideal para cargas de trabalho estáveis.
newCachedThreadPool() Pool dinâmico que cria novas threads conforme a necessidade e reutiliza as ociosas. Adequado para muitas tarefas curtas.
newSingleThreadExecutor() Garante que apenas uma thread execute as tarefas em ordem (FIFO/LIFO).
newScheduledThreadPool() Permite o agendamento de tarefas para execução futura ou periódica.

Veja um exemplo básico utilizando um pool fixo:


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

public class ExemploPoolFixo {
    public static void main(String[] args) {
        ExecutorService servico = Executors.newFixedThreadPool(3);

        for (int i = 1; i <= 5; i++) {
            final int idTarefa = i;
            servico.execute(() -> {
                String nomeThread = Thread.currentThread().getName();
                System.out.println("Tarefa " + idTarefa + " iniciando na " + nomeThread);
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
                System.out.println("Tarefa " + idTarefa + " finalizada.");
            });
        }
        servico.shutdown();
    }
}

Anatomia do ThreadPoolExecutor: Os 7 Parâmetros

Para um controle granular, utilizamos diretamente a classe ThreadPoolExecutor. Ela requer a configuração de sete parâmetros fundamentais:

  1. corePoolSize: Quantidade mínima de threads mantidas ativas, mesmo que estejam ociosas.
  2. maximumPoolSize: O limite máximo de threads que o pool pode criar caso a fila de tarefas fique cheia.
  3. keepAliveTime: Tempo máximo que threads excedentes (acima do core) podem esperar por novas tarefas antes de serem terminadas.
  4. unit: Unidade de tempo para o parâmetro anterior (ex: TimeUnit.SECONDS).
  5. workQueue: A fila (BlockingQueue) onde as tarefas aguardam antes de serem processadas.
  6. threadFactory: Interface usada para criar novas threads, permitindo customizar nomes, piroridades e status de Daemon.
  7. handler: A estratégia de rejeição aplicada quando o limite de threads e a capacidade da fila são atingidos.

As 4 Estratégias de Rejeição (RejectedExecutionHandler)

Quando o pool atinge sua capacidade máxima (threads máximas + fila cheia), ele invoca uma política de rejeição:

  • AbortPolicy (Padrão): Lança uma exceção RejectedExecutionException no momento da submissão.
  • CallerRunsPolicy: A tarefa é executada pela thread que tentou submetê-la (a thread "chamadora"), reduzindo a velocidade de novas submissões.
  • DiscardPolicy: A tarefa é silenciosamente descartada, sem nenhum aviso ou erro.
  • DiscardOldestPolicy: Descarta a tarefa mais antiga da fila de espera e tenta inserir a nova tarefa.

Implementando um Pool Customizado

No exemplo abaixo, configuramos um pool personalizado com uma fila limitada e uma política de rejeição específica:


import java.util.concurrent.*;

public class GerenciadorDeProcessamento {
    public static void main(String[] args) {
        int coreThreads = 2;
        int maxThreads = 4;
        long tempoEspera = 10;
        
        BlockingQueue<Runnable> filaTrabalho = new ArrayBlockingQueue<>(2);
        
        ThreadPoolExecutor executorCustomizado = new ThreadPoolExecutor(
                coreThreads,
                maxThreads,
                tempoEspera,
                TimeUnit.SECONDS,
                filaTrabalho,
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.CallerRunsPolicy()
        );

        for (int i = 0; i < 10; i++) {
            final int jobID = i;
            executorCustomizado.submit(() -> {
                System.out.println("Processando Job #" + jobID + " por " + Thread.currentThread().getName());
                try {
                    Thread.sleep(1500);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            });
        }

        executorCustomizado.shutdown();
    }
}

Neste cenário, se as 4 threads estiverem ocupadas e as 2 posições na fila estiverem preenchidas, a estratégia CallerRunsPolicy fará com que a thread main execute as tarefas excedentes, garantindo que nenhum dado seja perdido, mas diminuindo o ritmo de entrada de novos processamentos.

Tags: java Concurrency Multithreading thread-pool

Publicado em 7-23 08:57