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:
- corePoolSize: Quantidade mínima de threads mantidas ativas, mesmo que estejam ociosas.
- maximumPoolSize: O limite máximo de threads que o pool pode criar caso a fila de tarefas fique cheia.
- keepAliveTime: Tempo máximo que threads excedentes (acima do core) podem esperar por novas tarefas antes de serem terminadas.
- unit: Unidade de tempo para o parâmetro anterior (ex:
TimeUnit.SECONDS). - workQueue: A fila (
BlockingQueue) onde as tarefas aguardam antes de serem processadas. - threadFactory: Interface usada para criar novas threads, permitindo customizar nomes, piroridades e status de Daemon.
- 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
RejectedExecutionExceptionno 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.