No desenvolvimento de aplicações multithread em Java, a palavra-chave synchronized é a forma tradicional de garantir a exclusão mútua. No entanto, desde o JDK 1.5, a interface Lock e sua implementação principal, ReentrantLock, oferecem uma alternativa muito mais flexível e poderosa. Diferente do monitor implícito, o ReentrantLock permite recursos como a tentativa de bloqueio não bloqueante, notificações em múltiplas frentes e suporte a filas de espera justas.
Sincronização com ReentrantLock
O uso básico do ReentrantLock exige que o desenvolvedor invoque explicitamente o método lock() e, obrigatoriamente, o unlock() dentro de um bloco finally para garantir a liberação do recurso.
public class ProcessadorRecurso {
private final Lock trava = new ReentrantLock();
public void executarTarefa() {
trava.lock();
try {
for (int i = 1; i <= 3; i++) {
System.out.println(Thread.currentThread().getName() + " - Passo: " + i);
}
} finally {
trava.unlock();
}
}
}
public class Executor {
public static void main(String[] args) {
ProcessadorRecurso processador = new ProcessadorRecurso();
Runnable tarefa = processador::executarTarefa;
for (int i = 0; i < 3; i++) {
new Thread(tarefa).start();
}
}
}
Neste modelo, os threads executam o loop de forma atômica. Quando um thread adquire a trava, os outros permanecem em espera até que o unlock() seja chamado.
A Interface Condition: Wait e Notify Evoluídos
Enquanto o synchronized utiliza os métodos wait() e notify() do objeto, o ReentrantLock utiliza objetos Condition. A grande vantagem é a capacidade de criar múltiplos conjuntos de espera dentro do mesmo lock, permitindo notfiicações seletivas.
O erro comum: IllegalMonitorStateException
Um erro frequente é tentar chamar condition.await() sem possuir o bloqueio do lock associado. Assim como no wait(), o thread deve ser o dono do monitor antes de suspedner sua execução.
public class SistemaNotificacao {
private Lock lock = new ReentrantLock();
private Condition condicao = lock.newCondition();
public void aguardar() {
try {
lock.lock();
System.out.println("Thread entrando em espera...");
condicao.await();
System.out.println("Thread despertado!");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void sinalizar() {
lock.lock();
try {
condicao.signal();
} finally {
lock.unlock();
}
}
}
Notificação Seletiva com Múltiplas Condições
Ao contrário do notifyAll(), que acorda todos os threads parados no monitor, o uso de múltiplos objetos Condition permite que você acorde apenas threads de um grupo específico (ex: apenas produtores ou apenas consumidores).
public class GerenciadorFilas {
private Lock lock = new ReentrantLock();
private Condition condicaoA = lock.newCondition();
private Condition condicaoB = lock.newCondition();
public void processarA() {
lock.lock();
try {
System.out.println("Aguardando em A...");
condicaoA.await();
System.out.println("Finalizado A");
} catch (InterruptedException e) { e.printStackTrace(); }
finally { lock.unlock(); }
}
public void processarB() {
lock.lock();
try {
System.out.println("Aguardando em B...");
condicaoB.await();
System.out.println("Finalizado B");
} catch (InterruptedException e) { e.printStackTrace(); }
finally { lock.unlock(); }
}
public void despertarA() {
lock.lock();
try { condicaoA.signalAll(); }
finally { lock.unlock(); }
}
}
Locks Justos vs. Não Justos
O construtor do ReentrantLock aceita um parâmetro booleano para definir a "justiça" (fairness).
- Fair Lock: Garante que o thread que está esperando há mais tempo seja o próximo a adquirir o lock (FIFO).
- Non-fair Lock: Não garante ordem; threads novos podem "furar a fila", o que geralmente oferece maior throuhgput.
// Criando um lock justo
Lock travaJusta = new ReentrantLock(true);
Métodos de Monitoramento do Lock
A classe ReentrantLock oferece diversos métodos utilitários para depuração e controle de estado:
getHoldCount(): Retorna o número de vezes que o thread atual bloqueou este lock (reentrância).getQueueLength(): Estima o número de threads aguardando para adquirir o lock.getWaitQueueLength(Condition c): Retorna o número de threads aguardando uma condição específica.hasQueuedThreads(): Verifica se existem threads na fila de espera do lock.
Eficiência com ReentrantReadWriteLock
Em cenários onde as leituras são frequentes e as escritas raras, o ReentrantLock padrão pode ser um gargalo, pois impede leituras simultâneas. O ReentrantReadWriteLock resolve isso separando os bloqueios:
- Read Lock: Múltiplos threads podem ler simultaneamente se não houver ninguém escrevendo.
- Write Lock: Exclusividade total. Bloqueia tanto outros escritores quanto todos os leitores.
public class RepositorioDados {
private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
private String informacao = "Valor Inicial";
public void ler() {
rwLock.readLock().lock();
try {
System.out.println(Thread.currentThread().getName() + " lendo: " + informacao);
Thread.sleep(500);
} catch (InterruptedException e) { e.printStackTrace(); }
finally { rwLock.readLock().unlock(); }
}
public void atualizar(String novoValor) {
rwLock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName() + " atualizando dados...");
this.informacao = novoValor;
Thread.sleep(500);
} catch (InterruptedException e) { e.printStackTrace(); }
finally { rwLock.writeLock().unlock(); }
}
}
Com esta implementação, se dez threads chamarem ler() ao mesmo tempo, todos processarão em paralelo. No entanto, se um thread chamar atualizar(), ele aguardará o fim das leituras e impedirá novas leituras até concluir a escrita, garantindo a consistência dos dados.