Fundamentals 12 min read

Understanding Java's AbstractQueuedSynchronizer (AQS): Concepts, Internal Implementation, and Resource Acquisition/Release

This article explains the core concepts of Java's AbstractQueuedSynchronizer (AQS), its internal FIFO double‑linked list structure, key code snippets, how threads acquire and release resources in exclusive and shared modes, and provides practical insights for mastering Java concurrency.

Full-Stack Internet Architecture
Full-Stack Internet Architecture
Full-Stack Internet Architecture
Understanding Java's AbstractQueuedSynchronizer (AQS): Concepts, Internal Implementation, and Resource Acquisition/Release

What? You don't even know what AQS is? If you want to master Java concurrency, AQS is essential. Let's explore it together.

Basic Concepts

AQS stands for AbstractQueuedSynchronizer , which translates to "abstract queue synchronizer". The three words mean:

Abstract : AQS is an abstract class that implements the main logic while leaving some methods to subclasses.

Queued : It uses a FIFO (first‑in‑first‑out) queue to store data.

Synchronizer : It provides synchronization functionality.

In short, AQS is a framework for building locks and synchronizers, allowing you to construct synchronizers simply and efficiently.

AQS Internal Implementation

AQS maintains a FIFO double‑linked list internally. Each node in the list holds two pointers (previous and next), making it easy to traverse from any node to its predecessor or successor.

In AQS, each Node actually encapsulates a thread. When a thread fails to acquire a lock, it is wrapped into a Node and added to the AQS queue; when the lock is released, the head node wakes up a blocked Node (i.e., a thread).

AQS uses a volatile int state variable as the resource marker:

private volatile int state;

Subclasses can override getState() and setState() to implement custom logic. An important method is:

// compareAndSetState uses CAS to change the state
protected final boolean compareAndSetState(int expect, int update) {
    // See below for intrinsics setup to support this
    return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

The two crucial member variables are:

private transient volatile Node head;   // head node
private transient volatile Node tail;   // tail node

The waiting queue is a variant of the "CLH" lock queue. In AQS it is used for blocking synchronizers rather than spinlocks.

The Node class definition (kept unchanged) is:

static final class Node {
    /** Marker to indicate a node is waiting in shared mode */
    static final Node SHARED = new Node();
    /** Marker to indicate a node is waiting in exclusive mode */
    static final Node EXCLUSIVE = null;

    /** waitStatus value to indicate thread has cancelled */
    static final int CANCELLED =  1;
    /** waitStatus value to indicate successor's thread needs unparking */
    static final int SIGNAL    = -1;
    /** waitStatus value to indicate thread is waiting on condition */
    static final int CONDITION = -2;
    /** waitStatus value to indicate the next acquireShared should propagate */
    static final int PROPAGATE = -3;

    volatile int waitStatus;
    volatile Node prev;
    volatile Node next;
    volatile Thread thread;
    Node nextWaiter;

    final boolean isShared() {
        return nextWaiter == SHARED;
    }

    final Node predecessor() throws NullPointerException {
        Node p = prev;
        if (p == null) throw new NullPointerException();
        else return p;
    }

    Node() { }

    Node(Thread thread, Node mode) { // used by addWaiter
        this.nextWaiter = mode;
        this.thread = thread;
    }

    Node(Thread thread, int waitStatus) { // used by Condition
        this.waitStatus = waitStatus;
        this.thread = thread;
    }
}

AQS Resource Acquisition

The entry point for acquiring a resource is acquire(int arg) , where arg is the number of resources requested:

public final void acquire(int arg) {
    if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

If tryAcquire fails, the thread is wrapped into a Node and inserted into the queue via addWaiter(Node.EXCLUSIVE) :

private Node addWaiter(Node mode) {
    // generate a Node for the current thread
    Node node = new Node(Thread.currentThread(), mode);
    // try fast‑path CAS insertion at the tail
    Node pred = tail;
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    // fallback to full enqueue
    enq(node);
    return node;
}

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;
            }
        }
    }
}

After insertion, the thread repeatedly checks its predecessor; if the predecessor is the head and tryAcquire(arg) succeeds, it becomes the new head. Otherwise it may park (block) until it is unparked by its predecessor.

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();
            // If predecessor is head, try to acquire
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            // Otherwise, possibly park
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

Other acquisition methods include:

acquireInterruptibly : acquire in exclusive mode with interruptibility.

acquireShared : acquire in shared mode.

acquireSharedInterruptibly : acquire in shared mode with interruptibility.

AQS Resource Release

Releasing a resource is simpler. The core method is:

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

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);
}

The release logic wakes up the next waiting node, allowing the next thread to attempt acquisition.

Two Resource Sharing Modes

Exclusive mode : Only one thread can hold the resource at a time (e.g., ReentrantLock ).

Shared mode : Multiple threads can hold the resource simultaneously; the number of permits is defined by the argument (e.g., Semaphore , CountDownLatch ).

Now you should have a solid grasp of AQS and how it builds locks and synchronizers in Java.

JavaConcurrencythreadSynchronizationLockAQSAbstractQueuedSynchronizer
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.