O ecossistema de concorrência do Java oferece ferramentas poderosas para gerenciar o acesso a recursos compartilhados. No centro dessas ferramentas está a interface Lock, que proporciona uma flexibilidade superior ao uso da palavra-chave synchronized. O ReentrantLock é a implementação mais comum dessa interface, baseando-se no AbstractQueuedSynchronizer (AQS) para gerenciar o estado de sincronização e as filas de espera de threads.
A Interface Lock
Diferente do bloqueio intrínseco, a interface Lock permite tentativas de aquisição de bloqueio com interrupção, timeout e suporte a múltiplas condições. Abaixo, as principais operações definidas:
| Método | Funcionalidade |
|---|---|
lock() |
Bloqueia a thread atual até que o recurso seja adquirido. |
tryLock() |
Tenta obter o bloqueio imediatamente, retornando um booleano sem suspender a thread. |
unlock() |
Libera o recurso previamente adquirido pela thread. |
newCondition() |
Cria uma variável de condição vinculada ao bloqueio. |
Estrutura Hierárquica do ReentrantLock
O ReentrantLock utiliza uma classe interna abstrata chamada Sync, que estende o AQS. O AQS é um framework que facilita a criação de sincronizadores baseados em uma fila FIFO (First-In-First-Out), conhecida como fila CLH. A partir de Sync, derivam-se duas implementações específicas:
- NonfairSync: Prioriza a performance permitindo que threads recém-chegadas tentem "furar a fila".
- FairSync: Garante que a thread que está esperando há mais tempo tenha prioridade.
Funcionamento do Bloqueio Não Justo (Nonfair Lock)
Na estratégia não justa, quando o método lock() é invocado, o sistema tenta realizar uma operação atômica de CAS (Compare-And-Swap) imediatamente.
static final class SincronizadorNaoJusto extends Sync {
final void executarBloqueio() {
// Tenta adquirir o acesso de forma agressiva
if (compareAndSetState(0, 1)) {
definirThreadProprietaria(Thread.currentThread());
} else {
acquire(1); // Fluxo padrão do AQS
}
}
protected final boolean tryAcquire(int qtd) {
return tentativaNaoJusta(qtd);
}
}
Se o CAS falhar, a thread entra no fluxo acquire(1) do AQS. Este método tenta novamente obter o bloqueio e, se falhar, insere a thread na fila de espera. O método tentativaNaoJusta lida com a lógica de reentrada: se a thread que solicita o bloquieo já for a dona dele, o estado é incrementado.
final boolean tentativaNaoJusta(int valor) {
final Thread atual = Thread.currentThread();
int estadoAtual = getState();
if (estadoAtual == 0) {
if (compareAndSetState(0, valor)) {
setExclusiveOwnerThread(atual);
return true;
}
} else if (atual == getExclusiveOwnerThread()) {
int proximoEstado = estadoAtual + valor;
if (proximoEstado < 0) throw new Error("Excesso de reentrada");
setState(proximoEstado);
return true;
}
return false;
}
Gestão da Fila CLH e Suspensão de Threads
Quando a aquisição falha, o AQS utiliza o método addWaiter para encapsular a thread em um objeto Node e colocá-lo no final da fila. Em seguida, o método acquireQueued mantém a thread em um loop de verificação (spin) ou a suspende usando LockSupport.park().
final boolean gerenciarFila(final Node nodo, int valor) {
boolean falha = true;
try {
boolean interrompido = false;
for (;;) {
final Node anterior = nodo.predecessor();
// Se o anterior for a cabeça, tenta adquirir novamente
if (anterior == head && tryAcquire(valor)) {
setHead(nodo);
anterior.next = null; // Auxilia o Garbage Collector
falha = false;
return interrompido;
}
// Decide se a thread deve ser suspensa para economizar CPU
if (deveSuspender(anterior, nodo) && suspenderEVerificarInterrupcao()) {
interrompido = true;
}
}
} finally {
if (falha) cancelarAquisicao(nodo);
}
}
Processo de Liberação (Unlock)
A liberação de um bloqueio é feita através do método release do AQS. O passo fundamental é o tryRelease, que decrementa o estado de sincronização. Somente quando o estado chega a zero, o recurso é efetivamente liberado e a thread proprietária é removida.
protected final boolean tryRelease(int decremento) {
int novoEstado = getState() - decremento;
if (Thread.currentThread() != getExclusiveOwnerThread()) {
throw new IllegalMonitorStateException();
}
boolean liberadoTotalmente = false;
if (novoEstado == 0) {
liberadoTotalmente = true;
setExclusiveOwnerThread(null);
}
setState(novoEstado);
return liberadoTotalmente;
}
Após a liberação bem-sucedida, o AQS identifica o próximo nó válido na fila (o sucessor do head) e o desperta utilizando LockSupport.unpark(), permitindo que ele tente adquirir o bloqueio novamente no loop de acquireQueued.
Diferença entre Justo e Não Justo
A distinção reside no método tryAcquire. No modo justo (Fair Lock), o AQS verifica se existem threads esperando na fila antes de permitir que a thread atual tente um CAS através do método hasQueuedPredecessors(). No modo não justo, a thread tenta capturar o bloqueio imediatamente se ele estiver livre, o que reduz o overhead de troca de contexto, mas pode levar à inanição (starvation) de threads que já estão na fila.
O AQS transforma a complexidade da sincronização de threads em uma gestão eficiente de estado e filas, permitindo que o ReentrantLock opere de forma performática e robusta em ambientes altamente concorrentes.