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:
- AbortPolicy: Lança
RejectedExecutionException(padrão). - CallerRunsPolicy: A thread que chamou o método de execução processa a tarefa.
- DiscardOldestPolicy: Descarta a tarefa mais antiga na fila e tenta novamente.
- 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 * 2ou 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 umFuture, permitindo recuperar o resultado ou verificar o status da tarefa. Exceções são encapsuladas noFuturee lançadas apenas ao chamarget().
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.