AQS Deep Dive: How Java’s AbstractQueuedSynchronizer Powers Concurrency
This article dissects Java’s AbstractQueuedSynchronizer (AQS), explaining its three core components—volatile state, CAS, and a FIFO wait queue—how exclusive and shared modes operate, the step‑by‑step lock acquisition flow, ReentrantLock implementation details, fairness trade‑offs, and the underlying template‑method design pattern.
AQS’s Three Core Components
The central problem AQS solves is managing which thread holds a resource, which threads are queued, and how queued threads are awakened. It does this with three elements:
volatile int state – the resource’s status, whose meaning is defined by subclasses (e.g., lock re‑entrance count, semaphore permits, CountDownLatch count).
CAS – atomic compare‑and‑set operations ensure safe state transitions and queue modifications.
FIFO double‑linked wait queue – a CLH‑style queue of Node objects, each wrapping a waiting thread and a waitStatus field. The most important status is SIGNAL(-1), indicating that the predecessor must wake the node.
Exclusive vs. Shared Modes
Exclusive : acquire → tryAcquire for acquisition, release → tryRelease for release. Typical tools are ReentrantLock and write locks.
Shared : acquireShared → tryAcquireShared for acquisition, releaseShared → tryReleaseShared for release. Typical tools are Semaphore, CountDownLatch and read locks.
Methods prefixed with try are abstract hooks that subclasses implement to decide whether acquisition succeeds.
Step‑by‑Step Lock Acquisition (Exclusive Mode)
Following thread T2 attempting to acquire a lock already held by T1:
tryAcquire – T2 calls the subclass’s tryAcquire. CAS fails because state is 1.
addWaiter – T2 is wrapped in a Node and appended to the queue’s tail (initializing a sentinel head if needed).
acquireQueued – T2 checks if it is the first queued node; if not, it sets its predecessor’s waitStatus to SIGNAL and calls LockSupport.park(), entering WAITING state.
Release & Wake‑up – When T1 releases, it resets state to 0, sees the head’s waitStatus is SIGNAL, and invokes unparkSuccessor. T2 is unparked, retries tryAcquire, succeeds, becomes the new head, and proceeds.
LockSupport.park()/unpark() is the low‑level mechanism AQS uses for blocking, offering more flexibility than wait/notify because unpark can be called before park without losing the signal.
ReentrantLock on Top of AQS
ReentrantLock’s internal Sync class fills only two abstract methods:
protected boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(current);
return true;
}
} else if (current == getExclusiveOwnerThread()) {
setState(c + 1);
return true;
}
return false;
}This logic implements re‑entrancy (state counts lock acquisitions) and records the owning thread.
Fair vs. Non‑Fair Locks
By default new ReentrantLock() creates a non‑fair lock, which attempts a CAS before checking the queue, allowing new threads to “cut in line.” A fair lock ( new ReentrantLock(true)) first calls hasQueuedPredecessors() and only proceeds if no earlier thread is waiting. Non‑fair locks usually achieve higher throughput because they avoid the idle gap between a wake‑up and the awakened thread actually acquiring the lock, though they can cause starvation.
// Fair lock acquisition check
if (c == 0) {
if (!hasQueuedPredecessors() && compareAndSetState(0, 1)) {
setExclusiveOwnerThread(current);
return true;
}
}Template Method Pattern
AQS embodies the Template Method design pattern: the abstract class defines the invariant algorithm (queueing, parking, waking) while delegating the variable steps ( tryAcquire, tryRelease, etc.) to subclasses. Different synchronizers simply fill these hooks with tool‑specific logic, which is why many java.util.concurrent classes share the same underlying skeleton.
Key takeaways:
The heart of AQS is a volatile int state whose meaning is defined by each subclass.
Failed acquisition leads to a Node being enqueued; the predecessor’s SIGNAL status triggers LockSupport.park(), putting the thread in WAITING state.
Non‑fair locks default for higher throughput by allowing a thread to acquire before respecting queue order; fair locks preserve ordering at the cost of some performance.
AQS’s use of the Template Method pattern lets a wide range of concurrency utilities reuse the complex queue‑blocking‑wakeup logic while only implementing simple resource‑specific checks.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
