Sincronização Avançada em Java: Dominando ReentrantLock e ReentrantReadWriteLock

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:

  1. Read Lock: Múltiplos threads podem ler simultaneamente se não houver ninguém escrevendo.
  2. 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.

Tags: java Concorrência Multithreading ReentrantLock ReentrantReadWriteLock

Publicado em 7-25 13:48