How AbstractQueuedSynchronizer Powers Java’s Locking Primitives

The AbstractQueuedSynchronizer (AQS) provides a reusable infrastructure for building thread synchronization tools. Many concurrency utilities in java.util.concurrent — including ReentrantLock, Semaphore, and CountDownLatch — delegate their queuing and blocking logic to AQS. Understanding its internals reveals how these components guarantee correctness and performance.

Core State and Queue Architecture

AQS centers on two data structures: an atomic state integer and a doubly-linked node queue.

// Internal synchronization state; semantics depend on subclass
private volatile int state;

protected final int fetchState() {
    return state;
}

protected final void updateState(int updated) {
    state = updated;
}

// Compare-and-swap on state field
protected final boolean trySetState(int current, int next) {
    return UNSAFE.compareAndSwapInt(this, stateOffset, current, next);
}

Subclass interpretations of state vary:

  • ReentrantLock: 0 indicates unlocked; positive value counts re-entrant acquires.
  • Semaphore: remaining permits.
  • CountDownLatch: number of counts before gate opens.
  • ReentrantReadWriteLock: upper 16 bits track read holds, lower 16 bits track write holds.

The CLH Variant Queue

Waiting thread are held in a FIFO node list. Each node carries status flags, thread reference, and links to predecessor/successor.

abstract static class Node {
    static final Node EXCLUSIVE = null;   // mode marker
    static final Node SHARED = new Node();

    static final int CANCELLED  =  1;
    static final int AWAITING   = -1;     // successor needs signal
    static final int CONDITION  = -2;
    static final int SPREAD     = -3;     // shared propagation

    volatile int ws;            // wait condition
    volatile Node prev;
    volatile Node succ;
    volatile Thread owner;
    Node chainLink;             // mode or condition queue next
}

The head points to a node whose thread already holds the resource (used during release to wake the next waiter), while the tail receives newly blocked threads via CAS.

Design Principles: Template Methods and Extensibility

AQS separates general-purpose mechanisms (queue insertion, park/unpark, cancellation) from policy decisions (what qualifies as a successful/failed acquire). Template methods such as acquire and release call subclass hooks that define the policy:

Template (final) Hook (subclass override) Purpose
acquire(int arg) tryAcquire(int arg) Exclusive acquisition attempt
release(int arg) tryRelease(int arg) Exclusive release attempt
acquireShared(int arg) tryAcquireShared(int arg) Shared acquisition attempt
releaseShared(int arg) tryReleaseShared(int arg) Shared release attempt

By implementing only the try* hooks, developers define state management without dealing with thread suspension, queue maintenance, or cancellation.

Exclusive Mode Walkthrough (Using ReentrantLock)

When a thread invokes acquire(1):

  1. tryAcquire(1) is called. In ReentrantLock, this succeeds immediately if state == 0 (CAS to 1 and record owner), or if the current thread already holds the lock (increment state).
  2. On failure, the thread is wrapped into an EXCLUSIVE node and appended to the tail via CAS.
  3. The thread then spins, checking whether its predecessor is head and retrying tryAcquire.
  4. If the predecessor is not head or acquisition fails again, the predecessor’s wait status is marked AWAITING, and the thread parks itself via LockSupport.park.
  5. Upon release, the head node’s succesor is unparked, re-entering the spin-retry loop.

Release Mechanics

In release(1):

  1. tryRelease(1) decrements the state; if resulting state is 0 the owning thread is cleared and the method returns true.
  2. If the head node’s status indicates a waiting successor, that successor is unparked.
  3. The unparked thread resumes within the acquire loop and competes for the lock again.

Shared Mode Propagation

Shared mode accommodates multiple concurrent holders. When releaseShared succeeds, AQS may propagate wake-ups to several waiters. This is the foundation for Semaphore and CountDownLatch, where multiple threads can proceed once the gate opens.

Building a Simple Exclusive Lock with AQS

The following example implements a minimal re-entrant lock by extending AQS. Only tryAcquire and tryRelease need custom logic; queuing and blocking are inherited.

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;

class MiniLock implements Lock {

    private static final class Sync extends AbstractQueuedSynchronizer {
        @Override
        protected boolean tryAcquire(int acquires) {
            Thread caller = Thread.currentThread();
            int c = fetchState();
            if (c == 0) {
                if (trySetState(0, 1)) {
                    setExclusiveOwnerThread(caller);
                    return true;
                }
            } else if (getExclusiveOwnerThread() == caller) {
                updateState(c + 1);
                return true;
            }
            return false;
        }

        @Override
        protected boolean tryRelease(int releases) {
            if (getExclusiveOwnerThread() != Thread.currentThread())
                throw new IllegalMonitorStateException();
            int next = fetchState() - releases;
            boolean released = (next == 0);
            if (released) setExclusiveOwnerThread(null);
            updateState(next);
            return released;
        }
    }

    private final Sync sync = new Sync();

    public void lock()                { sync.acquire(1); }
    public void unlock()              { sync.release(1); }
    public void lockInterruptibly()   { sync.acquireInterruptibly(1); }
    public boolean tryLock()          { return sync.tryAcquire(1); }
    public boolean tryLock(long t, TimeUnit u) { return sync.tryAcquireNanos(1, u.toNanos(t)); }
    public Condition newCondition()   { return sync.new ConditionObject(); }
}

Usage:

MiniLock lock = new MiniLock();
new Thread(() -> {
    lock.lock();
    try { /* critical section */ } finally { lock.unlock(); }
}).start();

The subclass implements state transitions; all coordination with other threads, queue management, and parking/unparking is handled by the AQS framework.

Tags: java Concurrency JUC Locking

Posted on Fri, 17 Jul 2026 16:57:53 +0000 by vlcinsky