Padrão Singleton
O padrão Singleton assegura que uma classe possua apenas uma instância, fornecendo um ponto de acesso global. Para implementá-lo, o construtor da classe deve ser privado, impedindo a instanciação externa, e um método estático é responsável por retornar a instância única.
Existem duas abordagens principais para sua implementação:
- Instanciação Antecipada (Eager): O objeto é criado no momento do carregamento da classe. É inerentemente thread-safe, pois a JVM garante a segurança durante o class loading.
- Instanciação Tardia (Lazy): O objeto é criado apenas na primeira requisição. Para ambientes multithread, exige sincronização para evitar a criação de múltiplas instâncias.
A implementação Lazy thread-safe utiliza o padrão Double-Checked Locking e a palavra-chave volatile. O volatile previne reordenação de instruções pelo compilador e garante a visibilidade da memória entre threads. O primeiro if evita o custo de aquisição de lock após a instância ser criada, enquanto o segundo if previne a criação duplicada quando múltiplas threads tentam adquirir o monitor simultaneamente.
public class RecursoUnico {
private static volatile RecursoUnico instancia = null;
private RecursoUnico() {}
public static RecursoUnico obterInstancia() {
if (instancia == null) {
synchronized (RecursoUnico.class) {
if (instancia == null) {
instancia = new RecursoUnico();
}
}
}
return instancia;
}
}
Fila Bloqueante e o Modelo Produtor-Consumidor
Uma fila bloqueante é uma estrutura de dados thread-safe que suspende a thread produtora quando a fila está cheia e a thread consumidora quando a fila está vazia. Esse comportamento é fundamental para o modelo Produtor-Consumidor, que desacopla a geração e o processamento de dados, permitindo o controle de carga (achatar picos de requisições) em sistemas distribuídos.
Abaixo, uma implementação personalizada de uma fila circular bloqueante utilizando os métodos wait() e notifyAll() para coordenação entre threads.
public class FilaConcorrente {
private final Object[] elementos;
private int indiceInicio = 0;
private int indiceFim = 0;
private int contador = 0;
public FilaConcorrente(int capacidade) {
elementos = new Object[capacidade];
}
public synchronized void inserir(Object item) throws InterruptedException {
while (contador == elementos.length) {
wait();
}
elementos[indiceFim] = item;
indiceFim = (indiceFim + 1) % elementos.length;
contador++;
notifyAll();
}
public synchronized Object remover() throws InterruptedException {
while (contador == 0) {
wait();
}
Object item = elementos[indiceInicio];
indiceInicio = (indiceInicio + 1) % elementos.length;
contador--;
notifyAll();
return item;
}
}
Agendador de Tarefas
Um agandador permite executar tarefas após um determinado atraso. Internamenet, ele mantém uma fila de prioridade ordenada pelo tempo de execução e utiliza uma thread trabalhadora que monitora o topo da fila. Se o tempo da tarefa ainda não foi atingido, a thread entra em estado de espera pelo tempo restante, liberando a CPU para outros processos.
import java.util.PriorityQueue;
public class TarefaAgendada implements Comparable<TarefaAgendada> {
private final Runnable acao;
private final long tempoExecucao;
public TarefaAgendada(Runnable acao, long atraso) {
this.acao = acao;
this.tempoExecucao = System.currentTimeMillis() + atraso;
}
@Override
public int compareTo(TarefaAgendada outra) {
return Long.compare(this.tempoExecucao, outra.tempoExecucao);
}
public long getTempoExecucao() { return tempoExecucao; }
public Runnable getAcao() { return acao; }
}
public class AgendadorTarefas {
private final PriorityQueue<TarefaAgendada> fila = new PriorityQueue<>();
private final Object monitor = new Object();
public AgendadorTarefas() {
Thread worker = new Thread(() -> {
while (true) {
try {
synchronized (monitor) {
while (fila.isEmpty()) {
monitor.wait();
}
TarefaAgendada proxima = fila.peek();
long agora = System.currentTimeMillis();
if (agora >= proxima.getTempoExecucao()) {
proxima.getAcao().run();
fila.poll();
} else {
monitor.wait(proxima.getTempoExecucao() - agora);
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
});
worker.setDaemon(true);
worker.start();
}
public void agendar(Runnable acao, long atraso) {
synchronized (monitor) {
fila.offer(new TarefaAgendada(acao, atraso));
monitor.notify();
}
}
}
Pool de Threads
A criação e destruição frequente de threads gera alta sobrecarga para o sistema operacional. Um pool de threads mitiga esse problema mantendo um conjunto de threads pré-criadas que aguardam tarefas em uma fila compartilhada, promovendo a reutilização de recursos e melhorando a responsividade da aplicação.
Abaixo, uma implemetnação simplificada de um pool de threads utilizando uma BlockingQueue da biblioteca padrão para gerenciar as tarefas pendentes.
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
public class PoolExecucao {
private final BlockingQueue<Runnable> tarefas;
public PoolExecucao(int tamanhoPool) {
tarefas = new LinkedBlockingQueue<>();
for (int i = 0; i < tamanhoPool; i++) {
new Thread(() -> {
while (true) {
try {
Runnable tarefa = tarefas.take();
tarefa.run();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
}).start();
}
}
public void submeter(Runnable tarefa) {
tarefas.offer(tarefa);
}
}