Understanding Java AbstractQueuedSynchronizer (AQS) and Implementing a Custom Lock
This article explains the internal state management, waiting queue, and node states of Java's AbstractQueuedSynchronizer (AQS), demonstrates how to create a custom reentrant lock by extending AQS, and walks through the lock acquisition and release process with detailed code examples.
AQS is the abbreviation of AbstractQueuedSynchronizer .
AbstractQueuedSynchronizer Synchronization State
AbstractQueuedSynchronizer contains a state field that represents the synchronization status. The field is declared as:
private volatile int state;The state field is an int whose meaning is defined by subclasses. For example, in ReentrantLock , state == 0 means the lock is free, while state >= 1 indicates the lock is held by another thread.
Although AbstractQueuedSynchronizer does not implement the actual get/set logic for state , it provides template methods for subclasses to manipulate it.
Getting the State
protected final int getState() {
return state;
}Setting the State
protected final void setState(int newState) {
state = newState;
}CAS Updating the State
protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}AQS Waiting Queue
The waiting queue is a doubly‑linked list where each node has prev and next references pointing to its predecessor and successor.
Queue Node
Inside AbstractQueuedSynchronizer the waiting‑queue nodes are represented by the static inner class Node :
static final class Node {
...
}Node Modes
There are two node modes:
Exclusive node : only one thread may hold the resource at a time (e.g., ReentrantLock ).
Shared node : multiple threads may hold the resource concurrently (e.g., Semaphore ).
Node States
Nodes can be in five states:
CANCELLED : the thread was cancelled.
SIGNAL : the successor needs a wake‑up signal.
CONDITION : the thread is waiting on a condition.
PROPAGATE : in shared mode, the wake‑up signal propagates to other waiting threads.
Custom Synchronization Lock
To illustrate AQS, we implement a simple reentrant lock called CustomLock that extends AbstractQueuedSynchronizer and implements Lock . The subclass overrides tryAcquire and tryRelease and delegates lock and unlock to AQS's acquire and release methods.
public class CustomLock extends AbstractQueuedSynchronizer implements Lock {
@Override
protected boolean tryAcquire(int arg) {
int state = getState();
if (state == 0) {
if (compareAndSetState(state, arg)) {
setExclusiveOwnerThread(Thread.currentThread());
System.out.println("Thread: " + Thread.currentThread().getName() + " acquired the lock");
return true;
}
} else if (getExclusiveOwnerThread() == Thread.currentThread()) {
int nextState = state + arg;
setState(nextState);
System.out.println("Thread: " + Thread.currentThread().getName() + " re‑entered");
return true;
}
return false;
}
@Override
protected boolean tryRelease(int arg) {
int state = getState() - arg;
if (getExclusiveOwnerThread() != Thread.currentThread()) {
throw new IllegalMonitorStateException();
}
boolean free = false;
if (state == 0) {
free = true;
setExclusiveOwnerThread(null);
System.out.println("Thread: " + Thread.currentThread().getName() + " released the lock");
}
setState(state);
return free;
}
@Override
public void lock() {
acquire(1);
}
@Override
public void unlock() {
release(1);
}
...
}The lock is used in a demo program that starts two threads ("Thread A" and "Thread B") which both invoke runInLock to simulate concurrent access.
public class CustomLockSample {
public static void main(String[] args) throws InterruptedException {
Lock lock = new CustomLock();
new Thread(() -> runInLock(lock), "Thread A").start();
new Thread(() -> runInLock(lock), "Thread B").start();
}
private static void runInLock(Lock lock) {
try {
lock.lock();
System.out.println("Hello: " + Thread.currentThread().getName());
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}Acquiring the Resource (acquire)
The lock method calls acquire(1) . Its implementation is:
public final void acquire(int arg) {
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}Key steps:
CustomLock.tryAcquire(...) decides whether the current thread can obtain the resource.
addWaiter(...) appends the thread to the tail of the waiting queue.
acquireQueued(...) blocks the thread if it cannot acquire the lock.
selfInterrupt sets the thread's interrupt flag when necessary.
tryAcquire Purpose
In the base AQS class tryAcquire throws UnsupportedOperationException . Our CustomLock overrides it with the logic shown earlier, returning true when the thread can acquire the lock and false otherwise.
Thread B Contention Flow
When Thread A acquires the lock first, Thread B fails and is added to the queue. The queue then looks like:
Thread B Blocking
Thread B enters acquireQueued , which loops indefinitely until it becomes the head and tryAcquire succeeds. The loop uses shouldParkAfterFailedAcquire to decide whether to block and parkAndCheckInterrupt (which calls LockSupport.park ) to actually park the thread.
private final boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
return true;
if (ws > 0) {
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}Thread A Releases the Lock
When Thread A finishes, it calls unlock() , which invokes release(1) . The release method calls tryRelease (implemented in CustomLock ) to reset the state and, if successful, wakes up the successor thread via unparkSuccessor :
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next;
if (s != null)
LockSupport.unpark(s.thread);
}Thus Thread B is unparked, re‑enters the acquireQueued loop, finds that it is now at the head, and successfully acquires the lock. Setting the Queue Head After acquiring the lock, Thread B calls setHead(node) to make itself the new head and removes the previous head node: private void setHead(Node node) { head = node; node.thread = null; node.prev = null; } Thread B Releases the Lock Thread B releases the lock in the same way as Thread A. Since the queue is now empty, no further thread is woken. Summary By creating a CustomLock that extends AbstractQueuedSynchronizer and overriding tryAcquire and tryRelease , we walked through the complete lock acquisition and release process, examined the internal waiting‑queue mechanics, and demonstrated how AQS coordinates thread contention in Java.
Architecture Digest
Focusing on Java backend development, covering application architecture from top-tier internet companies (high availability, high performance, high stability), big data, machine learning, Java architecture, and other popular fields.
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.