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!
GO TO FULL VERSION