Fundamentals 17 min read

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.

Architecture Digest
Architecture Digest
Architecture Digest
Understanding Java AbstractQueuedSynchronizer (AQS) and Implementing a Custom Lock

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.

JavaConcurrencythreadLockAQSAbstractQueuedSynchronizerCustomLock
Architecture Digest
Written by

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.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.