Gerenciamento de Concorrência em Java com ReentrantLock

Fundamentos de Exclusão Mútua

A classe ReentrantLock fornece um mecanismo de bloqueio mais flexível e abrangente do que o modificador intrínseco synchronized. O uso básico exige que o desenvolvedor gerencie explicitamente a aquisição e a liberação do recurso.

public class SharedResource {
    private final Lock mutex = new ReentrantLock();

    public void executeCriticalTask() {
        mutex.lock();
        try {
            for (int i = 0; i < 3; i++) {
                System.out.println(Thread.currentThread().getName() + " processando item " + i);
            }
        } finally {
            // A liberação deve ocorrer obrigatoriamente no bloco finally
            mutex.unlock();
        }
    }
}

Para testar a exclusão mútua, múltiplas threads podem compartilhar a mesma instância. A saída demonstrará que a execução é serializada, garantindo que apenas uma thread por vez acesse a seção crítica.

Independência de Monitores

Uma diferença arquitetural crucial é que o ReentrantLock não utiliza o monitor intrínseco do objeto Java. Portanto, um bloqueio explícito e um bloco synchronized no mesmo objeto não competem entre si.

public class DualMonitorDemo {
    private final Lock explicitLock = new ReentrantLock();

    public void taskWithExplicitLock() {
        explicitLock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + " iniciou com ReentrantLock");
            Thread.sleep(3000);
            System.out.println(Thread.currentThread().getName() + " finalizou com ReentrantLock");
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            explicitLock.unlock();
        }
    }

    public synchronized void taskWithImplicitLock() {
        System.out.println(Thread.currentThread().getName() + " executou com synchronized");
    }
}

Se duas threads invocarem simultaneamente taskWithExplicitLock e taskWithImplicitLock, ambas prosseguirão de forma assíncrona, comprovando que operam em domínios de sincronização distintos.

Coordenação de Threads com Condition

Enquanto synchronized depende de wait() e notify(), o ReentrantLock utiliza a interface Condition. Isso permite que uma thread aguarde e libere o lock atomicamente.

public class SignalCoordinator {
    private final Lock lock = new ReentrantLock();
    private final Condition condition = lock.newCondition();

    public void waitForSignal() {
        lock.lock();
        try {
            System.out.println("Aguardando sinal...");
            condition.await();
            System.out.println("Sinal recebido, retomando execução.");
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            lock.unlock();
        }
    }

    public void sendSignal() {
        lock.lock();
        try {
            System.out.println("Enviando sinal...");
            condition.signal();
        } finally {
            lock.unlock();
        }
    }
}

Notificação Seletiva

A principal vantagem do Condition é a capacidade de criar múltiplas filas de espera para um único lock. Isso resolve o problema do "despertar aleatório" do notify(), permitindo notificações direcionadas.

public class BoundedBuffer {
    private final Lock lock = new ReentrantLock();
    private final Condition notFull = lock.newCondition();
    private final Condition notEmpty = lock.newCondition();
    
    private final Queue<String> queue = new LinkedList<>();
    private final int capacity = 5;

    public void produce(String item) {
        lock.lock();
        try {
            while (queue.size() == capacity) {
                notFull.await(); // Produtores aguardam se a fila estiver cheia
            }
            queue.add(item);
            System.out.println("Produzido: " + item + " | Tamanho: " + queue.size());
            notEmpty.signal(); // Notifica apenas os consumidores
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            lock.unlock();
        }
    }

    public void consume() {
        lock.lock();
        try {
            while (queue.isEmpty()) {
                notEmpty.await(); // Consumidores aguardam se a fila estiver vazia
            }
            String item = queue.poll();
            System.out.println("Consumido: " + item + " | Tamanho: " + queue.size());
            notFull.signal(); // Notifica apenas os produtores
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            lock.unlock();
        }
    }
}

API de Inspeção e Controle

Locks Justos e Injustos

O construtor do ReentrantLock aceita um booleano para definir a política de concessão. Locks justos (fair = true) garantem que a ordem de aquisição respeite a ordem de chegada na fila, enquanto o padrão (fair = false) permite que threads recém-chegadas "furarem a fila" para reduzir o tempo de inatividade (thread starvation).

public class FairnessDemo {
    // true para FairLock, false para UnfairLock (padrão)
    private final Lock fairLock = new ReentrantLock(true);

    public void execute() {
        fairLock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + " adquiriu o lock justo.");
        } finally {
            fairLock.unlock();
        }
    }
}

Métodos de Diagnóstico

A API expõe métodos úteis para depuração e monitoramento do estado do lock:

  • getHoldCount(): Retorna quantas vezes a thread atual adquiriu o lock (suporte a reentrância).
  • getQueueLength(): Estima o número de threads aguardando para adquirir o lock.
  • isFair(): Verifica se o lock foi configurado como justo.
  • hasQueuedThread(Thread t) e hasQueuedThreads(): Verificam se uma thread específica ou qualquer thread está na fila de espera.
  • isHeldByCurrentThread(): Confirma se a thread atual é a detentora do lock.
  • isLocked(): Indica se o lock está sendo mantido por alguma thread.

Aquisição Não-Bloqueante

Os métodos tryLock() e tryLock(long timeout, TimeUnit unit) tentam adquirir o lock sem bloquera a thread indefinidamente. Isso é fundamental para prevenir deadlocks e implementar lógicas de fallback.

public class SafeAcquisition {
    private final Lock lock = new ReentrantLock();

    public void performAction() {
        // Tenta adquirir o lock por no máximo 2 segundos
        if (lock.tryLock(2, TimeUnit.SECONDS)) {
            try {
                System.out.println("Lock adquirido com sucesso. Executando ação.");
                // Simulação de trabalho
                Thread.sleep(1000); 
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            } finally {
                lock.unlock();
            }
        } else {
            System.out.println("Falha ao adquirir o lock no tempo limite. Executando lógica alternativa.");
        }
    }
}

Comparativo: synchronized vs ReentrantLock

  • Implementação: synchronized é uma palavra-chave nativa da JVM (nível de sintaxe), enquanto ReentrantLock é uma classe da API Java (java.util.concurrent.locks) que exige chamadas explícitas de lock() e unlock().
  • Política de Concessão: synchronized atua estritamente como um lock injusto. ReentrantLock oferece a opção de alternar entre comportamento justo e injusto.
  • Flexibilidade de Notificação: O mecanismo de espera/notificação de synchronized (wait/notify) é limitado a um único conjunto de espera por objeto. ReentrantLock permite a criação de múltiplos objetos Condition, habilitando notificações seletivas e precisas.

Tags: java ReentrantLock Concorrência Multithreading Condition

Publicado em 9-29 20:25