Understanding ReentrantLock and the AQS Framework in Java
This article explains how Java's ReentrantLock works, detailing the role of the AbstractQueuedSynchronizer framework, the lock's internal architecture, the FIFO wait‑queue implementation, lock acquisition and release processes, and how these mechanisms avoid the herd effect in multithreaded environments.
ReentrantLock is a re‑entrant lock in Java, meaning that the thread which currently holds the lock can acquire it multiple times without blocking.
The lock is built on top of the AbstractQueuedSynchronizer (AQS) framework, which uses a volatile int state field together with low‑level Unsafe operations to atomically modify the lock state.
Internally, ReentrantLock defines an abstract inner class Sync that extends AQS, and provides two concrete subclasses NonfairSync and FairSync to implement non‑fair and fair locking policies respectively.
AQS maintains a FIFO waiting queue implemented as a doubly‑linked list. The queue has a dummy HEAD node (which does not hold a thread) and a TAIL node that points to the most recently enqueued thread.
When a thread calls lock() , ReentrantLock invokes acquire(int arg) . If tryAcquire(arg) fails, the thread is added to the queue via addWaiter(Node.EXCLUSIVE) and then enters acquireQueued to wait for the lock.
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}The addWaiter method creates a new Node for the current thread, attempts a CAS operation to link it after the current tail, and falls back to enq(node) if the tail is null.
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;
}The enq method initializes the queue when it is empty and then repeatedly attempts to CAS the new node onto the tail, ensuring only one thread succeeds at a time.
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}When the owning thread releases the lock, release(int arg) resets the state to zero and, if there are waiting nodes, calls unparkSuccessor to wake the first queued thread.
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}This FIFO queue design prevents the "herd effect"—the wasteful waking of many threads at once—by only unparking the thread at the head of the queue, a strategy also used in distributed lock implementations.
Overall, the article provides a step‑by‑step walkthrough of how ReentrantLock leverages AQS, how threads are enqueued and dequeued, and why the design yields efficient and fair synchronization in Java applications.
IT Architects Alliance
Discussion and exchange on system, internet, large‑scale distributed, high‑availability, and high‑performance architectures, as well as big data, machine learning, AI, and architecture adjustments with internet technologies. Includes real‑world large‑scale architecture case studies. Open to architects who have ideas and enjoy sharing.
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.