Desvendando os Mecanismos do AbstractQueuedSynchronizer e ReentrantLock

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.

Tags: java Concurrency AQS ReentrantLock Multithreading

Publicado em 8-5 03:45