Unlocking Java’s AQS: A Deep Dive into AbstractQueuedSynchronizer and Concurrency Primitives

This article explores the origins of Java's JUC package, explains the core concepts of AbstractQueuedSynchronizer—including template methods, exclusive and shared acquisition, CLH queues, CAS, and LockSupport—and demonstrates how locks, conditions, and synchronization mechanisms are implemented and used in real-world Java concurrency.

Su San Talks Tech
Su San Talks Tech
Su San Talks Tech
Unlocking Java’s AQS: A Deep Dive into AbstractQueuedSynchronizer and Concurrency Primitives

1. Origin of JUC

The synchronized keyword was originally implemented in JDK using heavyweight locks written in C++. Dissatisfied with its performance, Doug Lea created the JUC package to improve concurrency, focusing on AbstractQueuedSynchronizer (AQS) .

2. AQS Prerequisite Knowledge

2.1 Template Method

AbstractQueuedSynchronizer

is an abstract class that follows the Template Method pattern; subclasses must implement specific methods.

Template Method Definition: An abstract class defines the skeleton of an algorithm, allowing subclasses to override specific steps while the overall flow remains unchanged.
public abstract class SendCustom {
    public abstract void to();
    public abstract void from();
    public void date() { System.out.println(new Date()); }
    public abstract void send();
    public void sendMessage() {
        to();
        from();
        date();
        send();
    }
}

2.2 LockSupport

LockSupport

provides static methods to block and unblock threads. Common methods include park, parkNanos, parkUntil, and unpark. The metaphor is that a thread is a car; park stops it, unpark starts it.

2.3 CAS

Compare‑And‑Swap (CAS) is a CPU‑level atomic operation that provides lock‑free synchronization. It relies on the Unsafe class for hardware‑level compare‑and‑set.

3. Important AQS Methods

3.1 Exclusive Acquisition

The method acquire attempts to obtain the lock; if tryAcquire fails, the thread is added to the queue and may block.

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

must be implemented by subclasses (e.g., ReentrantLock) to handle fairness, re‑entrancy, and state checks.

3.1.1 acquire

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

3.1.2 acquireInterruptibly

Similar to acquire but responds to thread interruption.

public final void acquireInterruptibly(int arg) throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    if (!tryAcquire(arg))
        doAcquireInterruptibly(arg);
}

3.1.3 tryAcquireNanos

Attempts acquisition with a timeout, throwing InterruptedException if interrupted.

public final boolean tryAcquireNanos(int arg, long nanosTimeout) throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    return tryAcquire(arg) || doAcquireNanos(arg, nanosTimeout);
}

3.2 Shared Acquisition

Shared mode is used by constructs such as CountDownLatch and Semaphore. The method acquireShared follows a similar pattern but works with shared state.

public final void acquireShared(int arg) {
    if (tryAcquireShared(arg) < 0)
        doAcquireShared(arg);
}

3.3 Release

Exclusive release invokes tryRelease and, if successful, wakes the successor.

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

3.4 Shared Release

public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
        doReleaseShared();
        return true;
    }
    return false;
}

4. AQS Internals

4.1 CLH Queue

The CLH (Craig, Landin, Hagersten) queue is a FIFO linked list that provides fair, scalable spin‑lock behavior. Each thread enqueues a Node at the tail using CAS.

static final class Node {
    static final Node SHARED = new Node();
    static final Node EXCLUSIVE = null;
    volatile Node prev;
    volatile Node next;
    volatile Thread thread;
    volatile int waitStatus;
    // waitStatus values: CANCELLED=1, SIGNAL=-1, CONDITION=-2, PROPAGATE=-3, INITIAL=0
}

4.2 Wait Status

The waitStatus field encodes node state: CANCELLED (1), SIGNAL (-1), CONDITION (-2), PROPAGATE (-3), and INITIAL (0).

4.3 Fair vs. Non‑Fair Locks

Fair locks respect queue order; non‑fair locks may allow barging, generally offering higher throughput.

4.4 Lock/Unlock Flow

When a thread calls lock.lock(), it invokes acquire(1). If acquisition fails, the thread is enqueued and may park. Upon unlock(), release(1) is called, which may unpark the successor.

5. Specific Synchronizers

5.1 CountDownLatch

CountDownLatch

uses AQS in shared mode; each countDown() releases one shared permit.

public void countDown() {
    sync.releaseShared(1);
}

5.2 Semaphore

Semaphore maintains a permit count. Acquisition attempts to decrement the count; release increments it, potentially waking multiple waiting threads via setHeadAndPropagate.

5.3 ReentrantReadWriteLock

The lock uses a single 32‑bit int state: the high 16 bits track shared (read) count, the low 16 bits track exclusive (write) count. Each read lock thread has a HoldCounter to record re‑entrancy.

6. Condition Objects

Each Condition maintains its own wait queue separate from AQS's synchronization queue. Nodes in a condition queue have waitStatus = CONDITION and are transferred to the sync queue when signaled.

6.1 await

Calling await() releases the lock, enqueues the thread on the condition queue, and parks the thread until it is signaled.

6.2 signal / signalAll

signal()

moves the first waiting node to the sync queue; signalAll() transfers all waiting nodes.

private void doSignal() {
    Node first = firstWaiter;
    if (first != null)
        transferForSignal(first);
}

private void doSignalAll(Node first) {
    lastWaiter = firstWaiter = null;
    for (Node s = first; s != null; s = s.nextWaiter) {
        s.nextWaiter = null;
        transferForSignal(s);
    }
}

6.3 Practical Guidance

Because a lock can have many condition queues, use signal() when only one waiting thread should proceed, and reserve signalAll() for cases where all waiters must be awakened.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

javaconcurrencyLockAQSCondition
Su San Talks Tech
Written by

Su San Talks Tech

Su San, former staff at several leading tech companies, is a top creator on Juejin and a premium creator on CSDN, and runs the free coding practice site www.susan.net.cn.

0 followers
Reader feedback

How this landed with the community

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.