Fundamentals 29 min read

A Deep Dive into Java's AbstractQueuedSynchronizer (AQS) and Custom Mutex Implementation

This article explains the principles of Java's AbstractQueuedSynchronizer (AQS), compares semaphore and monitor mechanisms, analyzes the source code of ReentrantLock, and demonstrates how to create a custom non-reentrant mutex using AQS, with detailed diagrams and code examples.

Full-Stack Internet Architecture
Full-Stack Internet Architecture
Full-Stack Internet Architecture
A Deep Dive into Java's AbstractQueuedSynchronizer (AQS) and Custom Mutex Implementation

Preface

AQS (AbstractQueuedSynchronizer) is a framework for building locks and synchronizers; understanding its implementation is essential for Java concurrency and interview preparation.

Lock Principles – Semaphore vs Monitor

Concurrency problems consist of mutual exclusion and synchronization. Semaphore provides a counting mechanism with P and V operations, while a monitor (管程) encapsulates a shared resource, a condition variable, and entry/exit protocols, offering a more developer‑friendly model.

Semaphore

Semaphore (Semaphore) is an OS‑provided inter‑process communication method used to coordinate access to shared resources. It consists of a shared integer S and two atomic operations P and V.

Semaphore is represented by a shared integer S and two atomic operations PV; S can only be changed by P and V.

P operation (request): decrement S; if S < 0, the thread enters the waiting queue.

V operation (release): increment S; if S ≤ 0, a waiting thread is awakened.

Monitor

Dijkstra proposed in 1971 that all synchronization operations on a particular critical resource be centralized into a "secretary" process, forming a monitor. Threads must request the secretary to gain exclusive access to the critical resource.

A monitor improves on semaphore by grouping the paired P and V operations together and introducing condition variables, greatly reducing usage complexity.

Shared variables inside the monitor.

Condition variables inside the monitor.

Processes executing inside the monitor.

Statements that set initial values for the shared data.

Any thread wishing to access the shared resource must queue for the monitor, be checked, and wait until notified.

Semaphore and monitor are equivalent; each can be implemented using the other, but monitors are more developer‑friendly.

AQS Implementation Principles

AQS maintains a volatile state representing the shared resource and a FIFO CLH queue for waiting threads. It relies on CAS to guarantee atomic state updates.

For an exclusive lock, state is initialized to 0. A thread attempts to acquire the lock by CAS‑setting state to 1; if successful, it becomes the owner. If the CAS fails, the thread is enqueued via acquire(1) .

public abstract class AbstractQueuedSynchronizer {
    private volatile Node head;
    private volatile Node tail;
    private volatile int state;
    protected final boolean compareAndSetState(int expect, int update) {
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
    }
    // ... other methods ...
}

AQS Source Code Analysis

ReentrantLock, a commonly used lock, is built on AQS. Its lock() method first tries a non‑fair CAS acquisition; if it fails, it calls acquire(1) which enqueues the thread.

public final void lock() {
    if (!compareAndSetState(0, 1))
        acquire(1);
}

The tryAcquire method handles two cases: (1) state == 0 – the lock is free, so CAS succeeds and the thread becomes the owner; (2) the current thread already owns the lock, allowing re‑entrancy by incrementing state .

final boolean nonfairTryAcquire(int acquires) {
    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 acquisition fails, the thread is added to the CLH queue via addWaiter(Node.EXCLUSIVE) , which uses CAS to link the new node at the tail. When the queue is empty, a dummy head node is created.

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 core acquisition loop ( acquireQueued ) repeatedly checks whether its predecessor is the head; if so, it tries to acquire the lock. Otherwise it may park (block) the thread after setting the predecessor’s waitStatus to SIGNAL .

final boolean acquireQueued(final Node node, int arg) {
    boolean interrupted = false;
    for (;;) {
        final Node p = node.predecessor();
        if (p == head && tryAcquire(arg)) {
            setHead(node);
            p.next = null; // help GC
            return interrupted;
        }
        if (shouldParkAfterFailedAcquire(p, node) &&
            parkAndCheckInterrupt())
            interrupted = true;
    }
}

When a lock is released, AQS’s release method calls tryRelease (implemented in ReentrantLock’s Sync) and then unparks the successor of the head node.

public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

Custom Mutex Using AQS

By extending AbstractQueuedSynchronizer , a simple non‑reentrant mutex can be created with only three overridden methods.

public class Mutex {
    private final Sync sync = new Sync();
    public void lock() { sync.acquire(1); }
    public void unlock() { sync.release(1); }
    private static class Sync extends AbstractQueuedSynchronizer {
        @Override protected boolean tryAcquire(int arg) {
            return compareAndSetState(0, 1);
        }
        @Override protected boolean tryRelease(int arg) {
            setState(0);
            return true;
        }
        @Override protected boolean isHeldExclusively() {
            return getState() == 1;
        }
    }
}

This demonstrates how AQS abstracts the low‑level queue and state management, allowing developers to focus on the specific acquisition/release logic.

Conclusion

The article provides a step‑by‑step walkthrough of AQS’s internal mechanisms, its role in Java’s lock implementations, and how to build custom synchronizers. By relating concepts to real‑world analogies such as a hospital’s registration and treatment process, readers gain an intuitive understanding of mutual exclusion and synchronization in concurrent programming.

JavaconcurrencySynchronizationmutexLockAQSReentrantLock
Full-Stack Internet Architecture
Written by

Full-Stack Internet Architecture

Introducing full-stack Internet architecture technologies centered on Java

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.