O Coração do JUC: Desvendando o AbstractQueuedSynchronizer (AQS)

O AbstractQueuedSynchronizer (AQS) é a espinha dorsal do pacote java.util.concurrent (JUC). Ele fornece uma estrutura robusta para implementar sincronizadores dependentes de estados de bloqueio, como ReentrantLock, Semaphore e CountDownLatch.

Basicamente, o AQS gerencia dois aspectos críticos:

  • Template de Bloqueio: Define métodos que subclasses implemantam para ditar regras de aquisição (justa/injusta, exclusiva/compartilhada).
  • Gestão de Infraestrutura: Cuida de operações complexas como enfileiramento de threads, controle de suspensão (parking) e operações atômicas via CAS no estado do lock.

Atributos Fundamentais

A classe base gerencia o estado de sincronização através de uma variável volátil de 32 bits e uma fila do tipo FIFO (First-In-First-Out).

// Ponto de entrada da fila (sentinela ou nó que detém o lock)
private transient volatile Node head;

// Final da fila de espera
private transient volatile Node tail;

// Estado do sincronizador (0: livre, >0: ocupado ou reentrante)
private volatile int state;

Estrutura Interna: Classe Node

As threads que aguardam o acesso são encapsuladas em objetos Node, que compõem tanto a fila de sincronização quanto a fila de condição.

static final class Node {
    // Estados do nó (waitStatus):
    // 0: Estado inicial ao entrar na fila
    // 1 (CANCELLED): Thread desistiu devido a timeout ou interrupção
    // -1 (SIGNAL): O sucessor deste nó precisa ser acordado
    // -2 (CONDITION): O nó está em uma fila de condição (ConditionObject)
    // -3 (PROPAGATE): Usado em locks compartilhados para propagar liberações
    volatile int waitStatus;

    volatile Node prev; // Elo anterior na fila de sincronização
    volatile Node next; // Próximo elo na fila de sincronização
    volatile Thread thread; // A thread estacionada neste nó

    // Define se o nó busca modo SHARED (compartilhado) ou EXCLUSIVE (exclusivo)
    Node nextWaiter; 

    static final Node SHARED = new Node();
    static final Node EXCLUSIVE = null;
}

Fluxo de Operação: Modo Exclusivo

No modo exclusivo, apenas uma thread pode deter o sincronizador por vez. O método principal é o acquire.

public final void acquire(int valor) {
    // Tenta obter o lock via lógica da subclasse
    if (!tryAcquire(valor) && 
        acquireQueued(addWaiter(Node.EXCLUSIVE), valor)) {
        selfInterrupt();
    }
}

Enfileiramento de Threads

Se o tryAcquire falhar, a thread deve ser colocada na fila de forma segura utilizando CAS para evitar condições de corrida.

private Node addWaiter(Node modo) {
    Node novoNo = new Node(Thread.currentThread(), modo);
    Node ultimo = tail;
    
    if (ultimo != null) {
        novoNo.prev = ultimo;
        if (compareAndSetTail(ultimo, novoNo)) {
            ultimo.next = novoNo;
            return novoNo;
        }
    }
    // Inicializa a fila se estiver vazia ou se o CAS falhar
    inserirNaFila(novoNo);
    return novoNo;
}

private Node inserirNaFila(final Node no) {
    for (;;) {
        Node t = tail;
        if (t == null) {
            // Cria um nó "dummy" como cabeça inicial
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            no.prev = t;
            if (compareAndSetTail(t, no)) {
                t.next = no;
                return t;
            }
        }
    }
}

Suspensão e Espera Ativa

Uma vez na fila, a thread entra em um loop onde tenta adquirir o lock se for a próxima após o head, caso contrário, ela é suspensa.

final boolean acquireQueued(final Node no, int valor) {
    boolean erro = true;
    try {
        boolean interrompido = false;
        for (;;) {
            final Node anterior = no.predecessor();
            // Se o anterior for o head, tenta adquirir o recurso
            if (anterior == head && tryAcquire(valor)) {
                setHead(no); // O nó atual vira o novo head (dummy)
                anterior.next = null; // Ajuda o GC
                erro = false;
                return interrompido;
            }
            // Verifica se deve bloquear e suspende a thread
            if (deveBloquear(anterior, no) && estacionarVerificarInterrupcao())
                interrompido = true;
        }
    } finally {
        if (erro) cancelarAquisicao(no);
    }
}

Liberação de Recursos

Ao liberar o lock, o AQS deve notificar o próximo nó válido na fila para que ele possa retomar a execução.

public final boolean release(int valor) {
    if (tryRelease(valor)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            notificarSucessor(h);
        return true;
    }
    return false;
}

private void notificarSucessor(Node no) {
    int status = no.waitStatus;
    if (status < 0)
        compareAndSetWaitStatus(no, status, 0);

    Node proximo = no.next;
    // Se o próximo for nulo ou cancelado, busca do fim para o começo
    if (proximo == null || proximo.waitStatus > 0) {
        proximo = null;
        for (Node t = tail; t != null && t != no; t = t.prev)
            if (t.waitStatus <= 0)
                proximo = t;
    }
    if (proximo != null)
        LockSupport.unpark(proximo.thread);
}

Modo Compartilhado e Propagação

No modo compartilhado (ex: Semaphore ou ReadLock), quando uma thread adquire o recurso, ela pode desencadear a liberação de outras threads na fila que também buscam o modo compartilhado.

private void setHeadAndPropagate(Node no, int resultado) {
    Node h_antigo = head;
    setHead(no);
    
    // Se o resultado for positivo ou o status indicar sinalização, propaga
    if (resultado > 0 || h_antigo == null || h_antigo.waitStatus < 0) {
        Node s = no.next;
        if (s == null || s.isShared())
            doReleaseShared();
    }
}

Variáveis de Condição (ConditionObject)

O ConditionObject permite que threads esperem por condições específicas enquanto liberam o lock exclusivo. Ele utiliza uma fila separada (unidirecional).

  • await(): Adiciona a thread à fila de condição, libera totalmente o lock (suportando reentrância) e suspende a execução.
  • signal(): Move o primeiro nó da fila de condição de volta para a fila de sincronização principal do AQS para que ele dispute o lock novamente.

Visualização do Fluxo da Fila

  1. Estado Inicial: head e tail são nulos.
  2. Thread A ganha lock: O estado muda, mas a fila permanece vazia.
  3. Thread B falha e entra na fila:
    • Um nó sentinela (Head) é criado.
    • O nó da Thread B é conectado ao Head.
    • tail aponta para B.
  4. Thread A libera lock: O AQS olha para o head, identifica o sucessor (Thread B) e o acorda via unpark.
  5. Thread B assume: O nó de B torna-se o novo head, limpando sua referência de thread interna.

Tags: AQS Java-Concurrency Multi-threading Synchronization Mutex

Publicado em 6-27 00:15