Deep Dive into Java Concurrency: Inside the AQS Source Code (Part 4)
This article provides a thorough technical walkthrough of Java's AbstractQueuedSynchronizer, covering its core state and CLH queue structures, exclusive and shared lock acquisition/release processes, Condition implementation, and interview-ready explanations of its underlying mechanisms.
Introduction
In the previous three installments the author covered JMM, synchronized, volatile, and CAS as the low‑level building blocks of Java concurrency. This fourth part turns to the core framework that powers most synchronizers: AbstractQueuedSynchronizer (AQS).
What Is AQS?
AQS is the foundational framework for synchronizers in the java.util.concurrent package. It maintains a volatile int state and a CLH (Craig, Landin, and Hagersten) queue, which together implement the basic logic for blocking locks and other synchronizers.
Core Data Structures
The key fields are: volatile int state – the synchronization state, updated via CAS. For example, state = 0 means unlocked in ReentrantLock, a positive value is the re‑entrance count, the count value in CountDownLatch, or the remaining permits in Semaphore.
The Node class represents a waiting thread in the CLH queue. Important constants are CANCELLED = 1, SIGNAL = -1, CONDITION = -2, and PROPAGATE = -3. Each node holds waitStatus, prev, next, and the associated Thread.
Core Methods
acquire(int arg) – tries tryAcquire(arg); if it fails, the thread is enqueued via addWaiter(Node.EXCLUSIVE) and then spins in acquireQueued. If the thread is interrupted while waiting, it self‑interrupts before returning.
public final void acquire(int arg) {
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) {
selfInterrupt();
}
}addWaiter(Node mode) – creates a new Node for the current thread, attempts a fast‑path CAS insertion at the tail, and falls back to the full enq(node) algorithm when the fast path fails.
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);
return node;
}acquireQueued(Node node, int arg) – repeatedly checks the predecessor; when the predecessor becomes the head, it retries tryAcquire(arg). If acquisition fails, the thread may be parked until it is unblocked.
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null;
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) {
interrupted = true;
}
}
} finally {
if (failed) cancelAcquire(node);
}
}release(int arg) – invokes tryRelease(arg); if successful, it wakes the successor of the head node.
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}Shared Mode
In shared mode the framework uses acquireShared and releaseShared. The key difference is that a successful acquisition can be followed by other threads acquiring the same lock, and releasing propagates wake‑ups to subsequent nodes. The article shows a CountDownLatch example where await() calls acquireShared and threads are released when the count reaches zero.
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}Condition Implementation
The inner class ConditionObject implements the Condition interface. It maintains its own wait queue ( firstWaiter, lastWaiter) and provides await() (release lock, enqueue, block) and signal() (move a node to the sync queue and wake it).
public class ConditionObject implements Condition {
private Node firstWaiter;
private Node lastWaiter;
public final void await() throws InterruptedException {
// 1. enqueue, 2. release lock, 3. block, 4. reacquire
}
public final void signal() {
// move first waiter to sync queue and unpark
}
}Interview‑Ready Summary
Core idea: AQS combines state and a CLH queue to build synchronizers. state is a volatile int modified by CAS; subclasses implement tryAcquire / tryRelease to manipulate it.
The CLH queue is a doubly‑linked list of waiting Node objects.
AQS supports both exclusive and shared modes. acquire / release are template methods; the actual lock logic lives in subclass‑provided tryAcquire / tryRelease.
Built‑in ConditionObject supplies the classic wait/notify mechanism.
Next Episode Preview
The upcoming part will explore ReentrantLock and Condition in depth, covering fair vs. non‑fair locks, re‑entrancy principles, practical producer‑consumer scenarios, and more.
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.
Coder Trainee
Experienced in Java and Python, we share and learn together. For submissions or collaborations, DM us.
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.
