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:0indicates 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):
tryAcquire(1)is called. InReentrantLock, this succeeds immediately ifstate == 0(CAS to 1 and record owner), or if the current thread already holds the lock (incrementstate).- On failure, the thread is wrapped into an
EXCLUSIVEnode and appended to the tail via CAS. - The thread then spins, checking whether its predecessor is
headand retryingtryAcquire. - If the predecessor is not
heador acquisition fails again, the predecessor’s wait status is markedAWAITING, and the thread parks itself viaLockSupport.park. - Upon release, the head node’s succesor is unparked, re-entering the spin-retry loop.
Release Mechanics
In release(1):
tryRelease(1)decrements the state; if resulting state is 0 the owning thread is cleared and the method returnstrue.- If the head node’s status indicates a waiting successor, that successor is unparked.
- 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.