Understanding Java’s AbstractQueuedSynchronizer: The Core of JUC Concurrency
This article explains the design and implementation of Java’s AbstractQueuedSynchronizer (AQS), detailing its state management, CLH queue, Node structure, and how ReentrantLock (both fair and non‑fair) leverages AQS for lock acquisition and release, with concrete code examples and step‑by‑step analysis.
Overview
AQS (AbstractQueuedSynchronizer) is the foundation of Java Concurrency utilities. It provides a framework for building synchronizers that control access to shared resources. Classes such as ReentrantLock, Semaphore and CountDownLatch are implemented on top of AQS.
AQS maintains a volatile int state representing the synchronization status and an internal CLH double‑linked queue that orders waiting threads. Each contender is wrapped in a Node and enqueued via CAS. Threads that acquire the resource proceed to business logic; threads that cannot acquire are parked using LockSupport.park() until they are unparked.
AQS Structure
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {
private transient volatile Node head;
private transient volatile Node tail;
/** The synchronization state. */
private volatile int state;
...
}The inner static class Node stores the thread, its wait status, and links to predecessor and successor.
static final class Node {
volatile int waitStatus;
volatile Node prev;
volatile Node next;
volatile Thread thread;
Node nextWaiter;
...
}Possible waitStatus values:
CANCELLED (1) – node cancelled due to timeout or interrupt.
SIGNAL (-1) – predecessor will signal this node.
CONDITION (-2) – node waiting on a Condition, moves to sync queue after signal.
PROPAGATE (-3) – shared mode may wake further successors.
0 – default for a newly enqueued node.
ReentrantLock Implementation via AQS
Lock acquisition
Calling lock.lock() invokes NonfairSync.lock():
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}If the CAS succeeds, the thread becomes the owner. Otherwise acquire(1) is executed, which follows these steps: tryAcquire(arg) – attempts immediate acquisition. addWaiter(Node.EXCLUSIVE) – enqueues the thread at the tail of the CLH queue. acquireQueued(node, arg) – blocks the thread until it becomes head and can acquire.
If the thread is interrupted while waiting, selfInterrupt() records the interruption.
tryAcquire implementation (non‑fair)
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
final boolean nonfairTryAcquire(int acquires) {
final 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;
}If the current thread already holds the lock, the state is incremented (re‑entrancy). Otherwise the method returns false and the thread is queued.
addWaiter implementation
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;
}
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // initialize empty queue
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}acquireQueued implementation
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
} shouldParkAfterFailedAcquiresets the predecessor's waitStatus to SIGNAL so that it will wake the current node when it releases:
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
return true;
if (ws > 0) { // predecessor cancelled
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}Parking is performed via:
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}Unlocking
public void unlock() {
sync.release(1);
}
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev) {
if (t.waitStatus <= 0)
s = t;
}
}
if (s != null)
LockSupport.unpark(s.thread);
}When the owning thread releases the lock, tryRelease clears the owner and sets state to zero. unparkSuccessor then wakes the next valid node in the queue.
Fair vs. non‑fair locks
Creating a fair lock:
Lock lock = new ReentrantLock(true); static final class FairSync extends Sync {
final void lock() {
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
} else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
public final boolean hasQueuedPredecessors() {
Node t = tail; // read fields in reverse order of initialization
Node h = head;
Node s;
return h != t && ((s = h.next) == null || s.thread != Thread.currentThread());
}Fair locks always check hasQueuedPredecessors() before attempting CAS, ensuring that a thread cannot acquire the lock while earlier waiters exist. Non‑fair locks try CAS first, allowing a thread to “cut in line” if the lock is free.
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.
Shepherd Advanced Notes
Dedicated to sharing advanced Java technical insights, daily work snippets, and the power of persistent effort.
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.
