Why Is AQS the Cornerstone of Java Concurrency?
The article explains how Java's AbstractQueuedSynchronizer (AQS) serves as the foundational framework for most JUC utilities by encapsulating state, a FIFO wait queue, and template methods, and demonstrates its design through detailed source analysis of ReentrantLock, Semaphore, CountDownLatch, and a custom latch.
Introduction
Java developers use many concurrency utilities such as ReentrantLock, CountDownLatch, Semaphore and ReentrantReadWriteLock. Although their usage patterns differ, they all share a common underlying framework: AbstractQueuedSynchronizer (AQS).
What Is AQS?
AQS = a state + a wait queue + a set of template methods
AQS is an abstract class that cannot be used directly. Subclasses inherit the "skeleton" and implement the concrete synchronization logic. It encapsulates the most error‑prone parts of concurrent programming—thread queuing, blocking, waking, and contention—leaving only a few simple "fill‑in" methods for subclasses.
Analogy: AQS is like a standardized ticket hall. The hall contains a counter (remaining tickets), a FIFO queue (people waiting for tickets), and a calling rule (when a ticket is taken, the next person is called). Business‑specific rules (ticket price, who can buy, how many tickets) are defined by each concrete "ticket window" (subclass).
Core Components of AQS
1. Synchronization State
The core switch is a volatile int state. Its meaning is entirely defined by the subclass.
ReentrantLock: 0 = unlocked, >0 = lock held count (supports re‑entrance).
Semaphore: state = remaining permits.
CountDownLatch: state = remaining countdown.
ReentrantReadWriteLock: high 16 bits = read lock count, low 16 bits = write lock count.
Volatile guarantees visibility across threads, while CAS guarantees atomic updates.
2. CLH Wait Queue (Double‑Linked List)
When a thread cannot acquire the resource, it is placed into a FIFO double‑linked list. The node structure is:
static final class Node {
// shared mode sentinel
static final Node SHARED = new Node();
// exclusive mode sentinel (null)
static final Node EXCLUSIVE = null;
volatile int waitStatus;
volatile Node prev;
volatile Node next;
volatile Thread thread;
Node nextWaiter;
}The head is a dummy sentinel node that does not correspond to any waiting thread, which simplifies enqueue and dequeue operations.
Why a double‑linked list?
Enqueue only modifies the tail pointer – O(1) complexity.
Dequeue only modifies the head pointer – O(1) complexity.
When a node is cancelled it can be removed without breaking the list, preserving queue integrity.
3. Template Method Pattern
AQS implements all the heavy‑weight mechanics (queue management, blocking, waking) as final methods. Subclasses provide only five protected methods:
tryAcquire(int arg) tryRelease(int arg) tryAcquireShared(int arg) tryReleaseShared(int arg) isHeldExclusively()By default these methods throw UnsupportedOperationException; a subclass overrides only the ones it needs.
Core Workflow: acquire and release
Exclusive acquire (acquire)
public final void acquire(int arg) {
// 1. Try to acquire; if successful return.
// 2. If failed, enqueue and spin+CAS until enqueued.
// 3. Block in the queue; when woken, retry.
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}The three steps are:
tryAcquire(arg) : subclass‑specific attempt to grab the resource.
addWaiter(Node.EXCLUSIVE) : creates a Node and appends it to the tail.
acquireQueued(node, arg) : the thread spins briefly, then blocks until it is unparked.
The enqueue logic (simplified) is:
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node); // full enqueue path when fast‑path fails
return node;
}Exclusive release (release)
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}After a successful release, the next valid node is unparked via LockSupport.unpark, allowing it to compete for the resource again.
How AQS Powers Common JUC Utilities
1. ReentrantLock (exclusive mode)
The lock contains an inner Sync class that extends AQS. The non‑fair tryAcquire implementation:
final boolean nonfairTryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
} else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}The corresponding tryRelease resets the state to zero and clears the owner thread when the re‑entrance count drops to zero.
Fair vs. non‑fair:
Non‑fair: attempts CAS immediately; if it fails the thread joins the queue.
Fair: checks hasQueuedPredecessors() first; if there are waiting threads the current thread must queue.
2. Semaphore (shared mode)
Acquisition decrements the permit count if enough permits remain; otherwise it returns a negative value to indicate failure.
protected int tryAcquireShared(int acquires) {
for (;;) {
int available = getState();
int remaining = available - acquires;
if (remaining < 0)
return remaining; // not enough permits
if (compareAndSetState(available, remaining))
return remaining; // success, return remaining permits
}
}Release increments the permit count, checking for overflow.
protected boolean tryReleaseShared(int releases) {
for (;;) {
int current = getState();
int next = current + releases;
if (next < current) // overflow
throw new Error("Maximum permit count exceeded");
if (compareAndSetState(current, next))
return true;
}
}Analogy: a restaurant with ten tables (state = 10). Each group occupies a table (decrement), and when a table is freed the next waiting group is called.
3. CountDownLatch (shared mode)
Acquisition succeeds only when the count reaches zero.
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}Release (count‑down) decrements the state and returns true only on the transition to zero, which triggers a wake‑up of all waiting threads.
protected boolean tryReleaseShared(int releases) {
for (;;) {
int c = getState();
if (c == 0)
return false;
int nextc = c - 1;
if (compareAndSetState(c, nextc))
return nextc == 0; // true only when count hits zero
}
}The main thread calls await() and blocks; once all worker threads invoke countDown(), the latch opens and all waiting threads proceed.
4. Custom One‑Shot Latch (demonstration)
A minimal shared synchronizer where state = 0 means closed and state = 1 means opened.
public class OneShotLatch {
private final Sync sync = new Sync();
public void await() { sync.acquireShared(1); }
public void open() { sync.releaseShared(1); }
private static class Sync extends AbstractQueuedSynchronizer {
protected int tryAcquireShared(int arg) {
return getState() == 1 ? 1 : -1;
}
protected boolean tryReleaseShared(int arg) {
return compareAndSetState(0, 1);
}
}
}Test code launches three threads that call await(). After a one‑second pause the latch is opened, and all three threads are released simultaneously.
Why AQS Is the Cornerstone of Java Concurrency
Appropriate abstraction : It captures the universal pattern “resource competition + queue + release” without prescribing what the resource actually is. The single int state can represent a lock count, a permit count, a countdown, etc.
Template‑method mastery : The framework handles queuing, blocking, cancellation, and interruption. Subclasses answer only two questions – when can the resource be acquired (tryAcquire/tryAcquireShared) and when is it released (tryRelease/tryReleaseShared).
Performance‑focused design : Uses volatile + CAS for lock‑free state updates, a CLH FIFO queue to avoid the “thundering herd” problem, a spin‑then‑block strategy to balance CPU usage and latency, and LockSupport.park/unpark instead of the older wait/notify mechanism.
Dual‑mode support : Both exclusive and shared modes are built‑in, covering locks, semaphores, latches, read‑write locks, and even custom synchronizers. Combined with Condition queues, AQS can implement precise wait/notify semantics.
Because of this versatility, more than 90 % of the classes in java.util.concurrent are thin wrappers around AQS.
Conclusion
Understanding AQS gives a clear mental model for all JUC tools. The article walks through the source code of the core framework, shows how common utilities are built on top of it, and provides a hands‑on example of a custom synchronizer. Mastering AQS is the first step toward advanced Java concurrency programming.
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.
Java Tech Workshop
Focused on Java backend technologies, sharing fundamentals, multithreading, JVM, the Spring ecosystem, microservices, distributed systems, high concurrency, source‑code analysis, and practical experience. Continuously delivers high‑quality original content, interview guides, and learning roadmaps to help Java developers progress from beginner to advanced, enhancing technical skills and core competitiveness.
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.
