CodeGym /Cursos /JAVA 25 SELF /ReentrantLock e ReadWriteLock: diferenças e exemplos

ReentrantLock e ReadWriteLock: diferenças e exemplos

JAVA 25 SELF
Nível 52 , Lição 2
Disponível

1. Classe ReentrantLock: bloqueio flexível

A palavra-chave synchronized é ótima para casos básicos: de forma rápida e simples, você pode proteger um método ou um bloco de código. Mas às vezes queremos mais:

  • Controlar o bloqueio explicitamente (por exemplo, tentar adquiri-lo e, se não der, não esperar).
  • Separar permissões de “leitura” e “escrita” para um recurso.
  • Interromper a espera por um bloqueio.
  • Diagnosticar quem e quando adquiriu ou liberou o bloqueio.

Para essas tarefas existem as classes ReentrantLock e ReadWriteLock. Elas oferecem mais controle e recursos do que o bom e velho synchronized.

O que é isso, afinal?

ReentrantLock é uma classe que implementa a interface Lock. Ela funciona de forma semelhante ao synchronized, mas com recursos extras. A principal diferença é que o controle do bloqueio se torna explícito: você mesmo chama os métodos lock() e unlock().

Um ponto interessante: a palavra reentrant significa que a thread pode adquirir o mesmo lock várias vezes seguidas, sem criar deadlock. Isso é útil se o método se chama recursivamente ou trabalha com um lock comum em uma cadeia de chamadas.

Sintaxe de uso

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Counter {
    private int value = 0;
    private final Lock lock = new ReentrantLock();

    public void increment() {
        lock.lock(); // Adquire o lock
        try {
            value++;
        } finally {
            lock.unlock(); // Sempre libere o lock!
        }
    }

    public int getValue() {
        lock.lock();
        try {
            return value;
        } finally {
            lock.unlock();
        }
    }
}

Atenção:
As chamadas lock() e unlock() devem sempre ser acompanhadas por um bloco try...finally. Se você esquecer de chamar unlock(), nenhuma outra thread conseguirá entrar no bloco protegido — você terá um bloqueio eterno.

Recursos do ReentrantLock

Tentativa de aquisição:
Você pode tentar adquirir o lock, mas sem esperar indefinidamente:

if (lock.tryLock()) {
    try {
        // Processando
    } finally {
        lock.unlock();
    }
} else {
    // Não foi possível adquirir — faça outra coisa
}

Espera com timeout:

if (lock.tryLock(100, TimeUnit.MILLISECONDS)) {
    // Adquirido em 100 ms
}

Verificar se o lock está adquirido:

if (lock.isLocked()) { ... }

Diagnóstico da fila de espera, “justiça” do lock e outros extras.

3. Exemplo: incremento de contador com ReentrantLock

Vamos evoluir nosso aplicativo de console (por exemplo, simulando o processamento de pedidos a partir de várias threads). Vamos comparar como fica o trabalho com synchronized e com ReentrantLock.

Exemplo com synchronized

public class OrderCounter {
    private int count = 0;

    public synchronized void increment() {
        count++;
    }

    public synchronized int getCount() {
        return count;
    }
}

Exemplo equivalente com ReentrantLock

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class OrderCounter {
    private int count = 0;
    private final Lock lock = new ReentrantLock();

    public void increment() {
        lock.lock();
        try {
            count++;
        } finally {
            lock.unlock();
        }
    }

    public int getCount() {
        lock.lock();
        try {
            return count;
        } finally {
            lock.unlock();
        }
    }
}

Qual é o ganho?

  • É possível tentar adquirir o lock sem esperar indefinidamente (tryLock()).
  • É possível implementar lógica complexa: por exemplo, adquirir vários locks em uma ordem específica (relevante para estruturas de dados complexas).
  • É possível “desbloquear” em outro ponto (mas faça isso com muito cuidado — lembre-se sempre do unlock()!).

4. ReadWriteLock: bloqueio para leitura e escrita

O que é?

ReadWriteLock não é apenas um lock, e sim um distribuidor de acesso inteligente. Sua implementação principal é ReentrantReadWriteLock, que divide os bloqueios em duas categorias: para leitura e para escrita.

Quando as threads apenas leem os dados e ninguém altera nada, elas podem trabalhar juntas sem problemas — leitura não atrapalha leitura. Mas assim que alguém decide fazer uma alteração, todos os demais devem esperar: a escrita permite apenas um participante e exige exclusividade.

Essa abordagem é especialmente útil onde há muitas leituras e poucas alterações — por exemplo, em um catálogo de produtos que os usuários consultam constantemente, mas atualizam apenas de vez em quando.

Sintaxe de uso

import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ProductCatalog {
    private final Map<String, String> products = new HashMap<>();
    private final ReadWriteLock rwLock = new ReentrantReadWriteLock();

    public void addProduct(String id, String name) {
        rwLock.writeLock().lock();
        try {
            products.put(id, name);
        } finally {
            rwLock.writeLock().unlock();
        }
    }

    public String getProduct(String id) {
        rwLock.readLock().lock();
        try {
            return products.get(id);
        } finally {
            rwLock.readLock().unlock();
        }
    }
}

Exemplo de uso no nosso aplicativo

Suponha que temos uma base de pedidos que todas as threads leem (por exemplo, para buscar um pedido), mas de tempos em tempos chegam novos pedidos (operação de escrita).

import java.util.*;
import java.util.concurrent.locks.*;

public class OrderDatabase {
    private final List<String> orders = new ArrayList<>();
    private final ReadWriteLock rwLock = new ReentrantReadWriteLock();

    // Adição de pedido (requer writeLock)
    public void addOrder(String order) {
        rwLock.writeLock().lock();
        try {
            orders.add(order);
        } finally {
            rwLock.writeLock().unlock();
        }
    }

    // Obter uma cópia de todos os pedidos (leitura pode ser paralela)
    public List<String> getOrders() {
        rwLock.readLock().lock();
        try {
            // Retornamos uma cópia para evitar condição de corrida
            return new ArrayList<>(orders);
        } finally {
            rwLock.readLock().unlock();
        }
    }
}

O que está acontecendo?

  • Enquanto ninguém escreve, até mil threads podem ler a lista de pedidos simultaneamente.
  • Assim que uma thread começa a adicionar um pedido — as leituras são bloqueadas para evitar dados “parciais”.

5. Comparação: quando usar o quê?

Cenário synchronized ReentrantLock ReadWriteLock
Sincronização simples ✖ (excessivo)
Precisa de timeout/tentativa de aquisição
Muitas leituras, pouca escrita ✔ (ganho significativo)
Necessita diagnóstico/estatísticas
Bloqueio recursivo ✔ (reentrância)

Conclusão:

  • Para casos simples — use synchronized.
  • Para flexibilidade — ReentrantLock.
  • Para cenários “leitura frequente, escrita rara” — ReadWriteLock.

6. Visualização: diagrama de funcionamento do ReadWriteLock

flowchart LR
    subgraph Leitura
      T1[Thread 1] -- Leitura --> Orders
      T2[Thread 2] -- Leitura --> Orders
      T3[Thread 3] -- Leitura --> Orders
    end
    subgraph Escrita
      T4[Thread 4] -- Escrita (addOrder) --> Orders
    end
    Orders[Lista de pedidos]
    style Orders fill:#f9f,stroke:#333,stroke-width:2px

Enquanto nenhuma thread está escrevendo, todas podem ler simultaneamente. Assim que surge uma escrita, as demais threads aguardam a liberação do writeLock.

7. Particularidades de implementação e nuances

“Justiça” (fairness)

Em ReentrantLock e ReentrantReadWriteLock é possível habilitar o modo “justo” (fair mode): as threads são atendidas na ordem da fila, e não no esquema “quem chega primeiro, leva”. Isso evita “fome” de threads, mas pode reduzir o desempenho.

Lock fairLock = new ReentrantLock(true); // true — modo justo
ReadWriteLock fairRWLock = new ReentrantReadWriteLock(true);

Armadilhas potenciais

  • Esquecer o unlock: Se não chamar unlock(), você terá um bloqueio eterno. Sempre use try...finally.
  • Exceções dentro do lock: Mesmo que ocorra uma exceção dentro do bloco de código, o lock deve ser liberado!
  • Uso excessivo de ReadWriteLock: Para coleções pequenas ou quando quase sempre há escrita, faz pouco sentido usar ReadWriteLock, e o código se torna mais complexo.

8. Erros comuns

Erro nº 1: esqueceu de chamar unlock()
O erro mais comum e traiçoeiro é esquecer de chamar unlock() após adquirir o lock. Como resultado — bloqueio eterno, threads “penduradas”. Sempre use try...finally, mesmo que pareça que “nada pode dar errado aqui”.

Erro nº 2: usar ReadWriteLock onde não é necessário
Se você quase não tem leituras paralelas e a escrita é frequente, ReadWriteLock apenas complicará o código e reduzirá o desempenho. Use-o somente onde realmente há muitos leitores simultâneos.

Erro nº 3: adquirir múltiplos locks em ordens diferentes
Se seu código adquire vários Lock (por exemplo, para vários objetos), sempre faça isso na mesma ordem em todas as threads. Caso contrário, você pode obter deadlock — as threads vão esperar umas pelas outras para sempre.

Erro nº 4: tentar substituir synchronized por ReentrantLock “só porque sim”
Não vale a pena trocar todos os synchronized por Lock sem critério — isso nem sempre acelera o programa e pode tornar o código menos legível.

Erro nº 5: esquecer a reentrância
Se a mesma thread chama lock() várias vezes seguidas — isso é normal para ReentrantLock, mas não se esqueça de que unlock() precisa ser chamado o mesmo número de vezes!

Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION