Understanding ReentrantLock and AQS: A Deep Dive into Java Lock Implementation
This article provides a comprehensive walkthrough of Java's ReentrantLock implementation, covering linked list and queue data structures, the AbstractQueuedSynchronizer (AQS) framework, lock acquisition and release mechanisms, fair vs. non-fair locks, and detailed code analysis with illustrative diagrams.
Introduction
The article begins with a brief motivation to review linked list and queue data structures before diving into the source code of ReentrantLock .
Data Structures
Linked List
Explains singly, doubly, and circular linked lists using simple analogies, and shows diagrams of each structure.
Queue
Describes the FIFO nature of queues with everyday examples and includes an illustration.
AQS Queue Synchronizer
Introduces AbstractQueuedSynchronizer (AQS) as the core abstraction behind ReentrantLock , listing its key fields and abstract methods.
public abstract class AbstractQueuedSynchronizer {
private volatile Node head;
private volatile Node tail;
private volatile int state;
// ... other fields and methods ...
protected abstract boolean tryAcquire(int arg);
protected abstract boolean tryRelease(int arg);
// ...
}ReentrantLock Lock Process
Details the lock acquisition flow: construction of FairSync or NonfairSync , the lock() method delegating to sync.lock() , and the acquire algorithm that first calls tryAcquire and, if it fails, enqueues the thread.
public void lock() {
sync.lock();
}
private static final class Sync extends AbstractQueuedSynchronizer {
final void lock() { acquire(1); }
// abstract void lock(); implemented by FairSync / NonfairSync
}
private static final class FairSync extends Sync {
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;
}
}The addWaiter method creates a Node for the current thread and links it into the queue; enq initializes the queue when it is empty.
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) {
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}If the thread is not at the head of the queue, acquireQueued repeatedly checks its predecessor and may park the thread via LockSupport.park .
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);
}
}ReentrantLock Unlock Process
Unlock delegates to sync.release(1) , which calls tryRelease to decrement the state and, when the state reaches zero, clears the owner and wakes the next waiting thread.
public void unlock() { sync.release(1); }
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;
}The unparkSuccessor method wakes the first waiting thread after a successful release.
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);
}Additional Details
The article also explains hasQueuedPredecessors , which determines whether a thread must wait, and shouldParkAfterFailedAcquire , which sets the predecessor’s wait status to SIGNAL before parking.
public final boolean hasQueuedPredecessors() {
Node t = tail, h = head;
Node s;
return h != t && ((s = h.next) == null || s.thread != Thread.currentThread());
}Conclusion
Mastering ReentrantLock requires careful study of the AQS framework, the lock‑acquisition algorithm, and the subtle interactions of queue nodes; such low‑level knowledge is essential for backend developers who need precise control over concurrency.
Selected Java Interview Questions
A professional Java tech channel sharing common knowledge to help developers fill gaps. Follow 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.