Sincronização Explícita com ReentrantLock em Java
Além do bloqueio implícito fornecido pelo synchronized, a biblioteca padrão de Java oferece um mecanismo de bloqueio explícito: a interface Lock e sua principal implementação, a classe ReentrantLock. Ambas fazem parte do pacote java.util.concurrent.locks.
A lógica de um Lock é direta: antes de acessar um recurso compartilhado, uma thread deve primeiro adquirir o bloqueio (lock()). Se o bloqueio estiver livre, a thread o obtém e entra na seção crítica. Se outra thread já possuir o bloqueio, a thread atual fica em estado de espera. Após concluir o trabalho, é crucial que a thread libere o bloqueio (unlock()) para que outras possam prosseguir.
A interface Lock define métodos essenciais:
void lock(): Aguarda, se necessário, até que o bloqueio seja adquirido.boolean tryLock(): Tenta adquirir o bloqueio imediatamente, sem esperar. Retornatruese bem-sucedido efalsecaso contrário.void lockInterruptibly(): Adquire o bloqueio, mas permite que a thread em espera seja interrompida, oferecendo uma saída para longas esperas.void unlock(): Libera o bloqueio.Condition newCondition(): Retorna um objetoConditionassociado a esteLock, que oferece uma alternativa mais flexível aos métodoswait(),notify()enotifyAll().
A Classe ReentrantLock
O nome (Reentrant) refere-se a uma propriedade importante: a mesma thread que já possui o bloqueio pode adquiri-lo novamente, de forma recursiva, sem causar um deadlock consigo mesma. O bloqueio mantém um contador interno; para cada chamada a lock(), uma chamada correspondente a unlock() deve ser feita para liberar completamente o bloqueio.
Exemplo: Substituindo synchronized por ReentrantLock
Vamos adaptar o exemplo de condição de corrida anterior para usar ReentrantLock.
import java.util.concurrent.locks.ReentrantLock;
class CommonResource {
int x = 0;
}
class CountThread implements Runnable {
private CommonResource res;
private ReentrantLock locker;
CountThread(CommonResource res, ReentrantLock lock) {
this.res = res;
this.locker = lock;
}
public void run() {
locker.lock(); // 1. Adquire o bloqueio (bloqueia se necessário)
try {
// --- Início da Seção Crítica ---
res.x = 1;
for (int i = 1; i < 5; i++) {
System.out.printf("%s %d \n", Thread.currentThread().getName(), res.x);
res.x++;
Thread.sleep(100);
}
// --- Fim da Seção Crítica ---
} catch (InterruptedException e) {
System.out.println(e.getMessage());
} finally {
locker.unlock(); // 2. Libera o bloqueio
}
}
}
public class Program {
public static void main(String[] args) {
CommonResource commonResource = new CommonResource();
ReentrantLock locker = new ReentrantLock(); // Cria o objeto de bloqueio
for (int i = 1; i < 6; i++) {
Thread t = new Thread(new CountThread(commonResource, locker));
t.setName("Thread " + i);
t.start();
}
}
}O padrão de uso é sempre o mesmo: adquirir o bloqueio, executar o código crítico dentro de um bloco try, e liberar o bloqueio no bloco finally. Esta estrutura é obrigatória para garantir que o bloqueio seja liberado mesmo que ocorra uma exceção, prevenindo deadlocks.
O resultado da execução será ordenado, garantindo a exclusão mútua, assim como com synchronized. A ordem exata em que as threads adquirem o bloqueio pode variar:
Thread 1 1 Thread 1 2 Thread 1 3 Thread 1 4 Thread 2 1 Thread 2 2 Thread 2 3 Thread 2 4 Thread 3 1 ...
ReentrantLock vs. synchronized
Embora synchronized seja mais simples de usar, ReentrantLock oferece vantagens significativas:
- Flexibilidade: A aquisição e liberação do bloqueio podem ocorrer em métodos diferentes.
- Bloqueio não bloqueante: O método
tryLock()permite verificar se um bloqueio está disponível sem bloquear a thread, possibilitando a execução de lógicas alternativas. - Bloqueio interrompível:
lockInterruptibly()permite que uma thread pare de esperar pelo bloqueio se for interrompida, evitando esperas indefinidas. - Justiça (Fairness): O construtor
ReentrantLock(true)cria um bloqueio "justo", que concede acesso às threads na ordem em que chegaram, prevenindo starvation.
Quando usar qual? Para a maioria dos cenários de sincronização simples, a sintaxe concisa do synchronized é suficiente e preferível. No entanto, para cenários mais complexos que exigem as funcionalidades avançadas listadas acima, ReentrantLock é a ferramenta mais adequada.
Resumo
- A classe
ReentrantLocké uma alternativa explícita e flexível aosynchronized. - Ela oferece funcionalidades avançadas como bloqueios não bloqueantes (
tryLock()) e interrompíveis (lockInterruptibly()). - O termo Reentrant significa que uma thread pode adquirir o mesmo bloqueio múltiplas vezes sem se autobloquear.
- É obrigatório liberar o bloqueio com
unlock()dentro de um blocofinallypara garantir a segurança e evitar deadlocks.