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. Retornatrue
se bem-sucedido efalse
caso 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 objetoCondition
associado 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 blocofinally
para garantir a segurança e evitar deadlocks.