Fundamentals 37 min read

How Does AQS Queue Threads? CLH Queue, State & Condition Variables Deep Dive

This article provides a comprehensive source-code-level analysis of Java's AbstractQueuedSynchronizer (AQS), detailing its three core components—state, CLH wait queue, and ConditionObject—and walking through exclusive lock acquisition/release, fair vs non-fair locking, shared lock propagation, and condition variable await/signal mechanics with code examples.

Java Tech Workshop
Java Tech Workshop
Java Tech Workshop
How Does AQS Queue Threads? CLH Queue, State & Condition Variables Deep Dive

1. What Is AQS?

AQS (AbstractQueuedSynchronizer) is the core foundation of the JUC (java.util.concurrent) package. Almost all concurrency utilities are built on it:

ReentrantLock : uses Sync (NonfairSync / FairSync); state meaning: lock reentry count (0 = unlocked, n = reentered n times)

Semaphore : uses Sync (NonfairSync / FairSync); state meaning: remaining permits

CountDownLatch : uses Sync; state meaning: remaining count (0 = latch open)

ReentrantReadWriteLock : ReadLock / WriteLock share Sync; state meaning: high 16 bits = read lock count, low 16 bits = write lock reentry count

ThreadPoolExecutor : Worker extends AQS; state meaning: worker thread state (0 = interruptible, 1 = locked)

FutureTask : internal Sync (deprecated, now uses state directly); state meaning: task state

In one sentence: AQS is a "synchronization queue framework" that provides generic thread queuing, parking, and unparking mechanisms. Subclasses only need to implement tryAcquire / tryRelease etc., define the meaning of state , and can quickly implement a concurrency tool.

1.2 Template Method Pattern

AQS uses the classic Template Method pattern:

AQS implements generic queuing, parking, unparking logic ( acquire, release, addWaiter, acquireQueued, etc.).

Subclasses (e.g., ReentrantLock.Sync) only implement a few abstract/overridable methods to define "how to acquire/release the lock".

Methods subclasses must implement (per mode): tryAcquire(int): try to acquire lock — required for exclusive mode, not needed for shared mode tryRelease(int): try to release lock — required for exclusive mode, not needed for shared mode tryAcquireShared(int): try to acquire shared lock — not needed for exclusive mode, required for shared mode tryReleaseShared(int): try to release shared lock — not needed for exclusive mode, required for shared mode isHeldExclusively(): whether exclusively held — recommended for both modes

AQS-provided template methods (no need to override): acquire(int): exclusive acquire (non-interruptible) acquireInterruptibly(int): exclusive acquire (interruptible) release(int): exclusive release acquireShared(int): shared acquire releaseShared(int): shared release tryAcquireNanos(int, long): exclusive timed acquire

2. AQS Three Core Components: state, CLH Queue, Condition Variables

2.1 state: Synchronization State

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {

    // Synchronization state, volatile for visibility
    private volatile int state;

    protected final int getState() { return state; }
    protected final void setState(int newState) { state = newState; }

    // CAS modify state for atomicity
    protected final boolean compareAndSetState(int expect, int update) {
        return U.compareAndSwapInt(this, STATE, expect, update);
    }
}
state

is a volatile int; its meaning is defined by subclasses: ReentrantLock: 0 = unlocked, n = thread reentered n times. Semaphore: remaining permits. CountDownLatch: remaining count, 0 = latch open. ReentrantReadWriteLock: high 16 bits = read lock count, low 16 bits = write lock reentry count.

Key points: volatile guarantees visibility across threads. compareAndSetState uses CAS for atomicity (lock-free modification). state is the sole basis for AQS to determine "is the lock held" — subclasses modify state to represent acquire/release.

2.2 CLH Queue: Thread Wait Queue

Threads that fail to acquire the lock are wrapped as Node objects and added to a doubly-linked list queue (CLH queue) to wait.

// AQS queue head and tail
private transient volatile Node head;
private transient volatile Node tail;

// Node class
static final class Node {
    // Node mode: shared / exclusive
    static final Node SHARED = new Node();
    static final Node EXCLUSIVE = null;

    // Wait status (waitStatus)
    static final int CANCELLED = 1;   // Node cancelled (timeout/interrupt)
    static final int SIGNAL    = -1;  // Successor needs to be unparked
    static final int CONDITION = -2;  // Node in condition queue
    static final int PROPAGATE = -3;  // Propagate wake-up in shared mode

    volatile int waitStatus;
    volatile Node prev;
    volatile Node next;
    volatile Thread thread;
    Node nextWaiter; // Next node in condition queue (or SHARED/EXCLUSIVE marker)
}

CLH queue structure:

head (virtual node, no thread)          tail
         │                                      │
         ▼                                      ▼
    ┌──────────┐   next   ┌──────────┐   ┌──────────┐
    │  Node 1  │ ───────► │  Node 2  │ ──►│  Node 3  │
    │ thread=A │ ◄─────── │ thread=B │ ◄──│ thread=C │
    │ ws=SIGNAL│   prev   │ ws=SIGNAL│    │ ws=0     │
    └──────────┘          └──────────┘    └──────────┘

Key points:

head is a virtual node (sentinel), does not store a waiting thread; it represents the "currently lock-holding thread".

New nodes are appended after tail, becoming the new tail.

On lock release, the successor of head (next) is unparked; the awakened thread acquires the lock and becomes the new head. waitStatus determines whether a thread needs to be unparked or has been cancelled.

Why "CLH"? CLH stands for Craig, Landin, and Hagersten, who proposed a linked-list-based spin lock. AQS's queue is a variant of the CLH lock: the original CLH queue is singly-linked; AQS changed it to a doubly-linked list (to safely remove cancelled nodes) and uses park / unpark instead of spinning.

2.3 Condition Variables: ConditionObject

AQS provides condition variables ( ConditionObject), implementing wait/notify mechanics similar to Object.wait / notify but more flexible (multiple condition queues per lock).

// AQS inner class
public class ConditionObject implements Condition, java.io.Serializable {
    private transient Node firstWaiter; // Condition queue head
    private transient Node lastWaiter;  // Condition queue tail

    // Wait (like Object.wait)
    public final void await() throws InterruptedException { ... }

    // Signal one (like Object.notify)
    public final void signal() { ... }

    // Signal all (like Object.notifyAll)
    public final void signalAll() { ... }
}

The condition queue is a singly-linked list (linked via nextWaiter), separate from the CLH sync queue:

On await: release lock → move from sync queue to condition queue → park to suspend.

On signal: remove head node from condition queue → append to sync queue tail → unpark to wake (will compete for lock after it's released).

Condition queue (singly): firstWaiter → Node → Node → lastWaiter
Sync queue (doubly):      head ⇄ Node ⇄ Node ⇄ tail

3. Exclusive Lock Full Flow: From acquire to release

Using ReentrantLock 's exclusive lock as example.

3.1 Acquire Lock: acquire

ReentrantLock.lock()

calls AQS's acquire(1):

// AQS.acquire()
public final void acquire(int arg) {
    // 1. Try to acquire lock (subclass implementation)
    if (!tryAcquire(arg) &&
    // 2. Failed: create node, enqueue, then spin-wait
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        // 3. If interrupted during wait, set interrupt flag
        selfInterrupt();
}

Three steps: tryAcquire(arg): attempt to acquire lock; success returns immediately (subclass implements, e.g., ReentrantLock.NonfairSync uses CAS on state).

On failure, addWaiter(Node.EXCLUSIVE): wrap current thread as Node, append to CLH queue tail. acquireQueued(node, arg): spin in queue, re-attempt lock after being unparked.

3.2 Enqueue: addWaiter

private Node addWaiter(Node mode) {
    // 1. Create node, bind current thread
    Node node = new Node(Thread.currentThread(), mode);

    // 2. Fast path: CAS node after tail
    Node pred = tail;
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) { // CAS set tail
            pred.next = node;
            return node;
        }
    }

    // 3. CAS failed (queue not initialized or contention), enter enq spin
    enq(node);
    return node;
}

private Node enq(final Node node) {
    for (;;) { // spin until success
        Node t = tail;
        if (t == null) { // queue not initialized
            if (compareAndSetHead(new Node()))
                tail = head;
        } else { // queue initialized, CAS node after tail
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

Key points:

Fast CAS insertion first; on failure, spin in enq (optimization: reduce spin count).

Queue initialization creates a virtual head node (no thread), hence head is a sentinel.

Enqueue uses CAS on tail for thread safety.

3.3 Wait in Queue: acquireQueued

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) { // spin
            // 1. Get predecessor
            final Node p = node.predecessor();

            // 2. If predecessor is head, we are first in line, try acquire
            if (p == head && tryAcquire(arg)) {
                // Success: become new head
                setHead(node);
                p.next = null; // old head GC
                failed = false;
                return interrupted;
            }

            // 3. Failed, decide whether to park
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node); // cancel on exception
    }
}

Core logic:

Only when predecessor is head does the thread attempt to acquire (ensures FIFO order).

On success, thread becomes new head ( setHead), old head is discarded.

On failure, shouldParkAfterFailedAcquire decides whether to park.

3.4 Decide Parking: shouldParkAfterFailedAcquire

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;

    if (ws == Node.SIGNAL) {
        // Predecessor state is SIGNAL → predecessor will unpark me on release, safe to park
        return true;
    }

    if (ws > 0) { // CANCELLED = 1
        // Predecessor cancelled, skip it, walk backwards
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        // Predecessor state 0 or PROPAGATE, CAS set predecessor to SIGNAL
        // (tell predecessor: when you release, unpark me)
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false; // don't park this round, spin again
}

Key points: SIGNAL means: "When I release the lock, I will unpark my successor".

After enqueuing, the node sets its predecessor's waitStatus to SIGNAL, so the predecessor knows to unpark it on release.

If predecessor is cancelled ( CANCELLED), skip backwards to find a valid predecessor.

Setting SIGNAL defers parking to the next spin iteration (gives one retry chance, avoids unnecessary park).

3.5 Park Thread: parkAndCheckInterrupt

private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this); // block current thread until unpark or interrupt
    return Thread.interrupted(); // return whether interrupted, clear flag
}
LockSupport.park()

is AQS's underlying park mechanism, based on Unsafe.park(). It blocks until:

Unparked via unpark.

Interrupted.

Spurious wakeup (hence the spin loop re-check).

3.6 Release Lock: release

public final boolean release(int arg) {
    // 1. Try to release lock (subclass implementation)
    if (tryRelease(arg)) {
        Node h = head;
        // 2. Head not null and waitStatus != 0 (successor needs wakeup)
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h); // unpark successor
        return true;
    }
    return false;
}

3.7 Unpark Successor: unparkSuccessor

private void unparkSuccessor(Node node) {
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0); // clear head status to 0

    // 1. Find successor
    Node s = node.next;
    if (s == null || s.waitStatus > 0) { // successor null or cancelled
        s = null;
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t; // walk from tail backwards to first valid node
    }

    // 2. Unpark successor's thread
    if (s != null)
        LockSupport.unpark(s.thread);
}

Key points:

On release, unpark head's successor.

If successor cancelled, walk from tail backwards to find first valid node (why backwards? because prev is set before next during enqueue, so backward traversal is safer).

After unpark, the awakened thread returns from parkAndCheckInterrupt, continues spinning, attempts tryAcquire.

3.8 Exclusive Lock Complete Flow Diagram

Thread A calls lock()
    │
    ▼
tryAcquire(1) ──success──► Got lock, return
    │fail
    ▼
addWaiter(EXCLUSIVE) → wrap Node, CAS enqueue at tail
    │
    ▼
acquireQueued(node, 1) ←──────────┐
    │                             │
    ▼                             │
Predecessor is head?──no──► shouldParkAfterFailedAcquire
    │yes                        │ set predecessor to SIGNAL
    ▼                           │
tryAcquire(1) ──fail──► parkAndCheckInterrupt → park suspend
    │success                    │
    ▼                           │   unparked by unpark
setHead(node), become new head ──────────┘
    │
    ▼
Return, hold lock


Thread B calls unlock()
    │
    ▼
tryRelease(1) ──fail──► return false
    │success
    ▼
unparkSuccessor(head) → unpark head's successor (Thread A)
    │
    ▼
Thread A woken, continues spin, tryAcquire succeeds, becomes new head

4. Fair vs Non-Fair Lock: AQS-Level Difference

ReentrantLock

has fair and non-fair implementations; the difference lies in tryAcquire.

4.1 Non-Fair Lock (Default)

static final class NonfairSync extends Sync {
    final void lock() {
        // Immediately CAS grab lock, ignore queue
        if (compareAndSetState(0, 1))
            setExclusiveOwnerThread(Thread.currentThread());
        else
            acquire(1);
    }

    protected final boolean tryAcquire(int acquires) {
        // Non-fair: if state == 0, grab directly, no queue check
        if (getState() == 0) {
            if (compareAndSetState(0, acquires)) {
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
        }
        // Reentrant: current thread already holds lock, state + acquires
        else if (Thread.currentThread() == getExclusiveOwnerThread()) {
            int nextc = getState() + acquires;
            setState(nextc);
            return true;
        }
        return false;
    }
}

Non-fair characteristics: lock() immediately CAS grabs lock, no queuing. tryAcquire grabs if state == 0, ignoring waiting threads.

Pros: higher throughput (new threads can grab lock without queuing).

Cons: may cause starvation of queued threads (new threads keep stealing lock).

4.2 Fair Lock

static final class FairSync extends Sync {
    final void lock() {
        acquire(1); // no barging, directly queue
    }

    protected final boolean tryAcquire(int acquires) {
        if (getState() == 0) {
            // ✅ Fair: first check if anyone queued (hasQueuedPredecessors)
            if (!hasQueuedPredecessors() &&
                compareAndSetState(0, acquires)) {
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
        }
        else if (Thread.currentThread() == getExclusiveOwnerThread()) {
            int nextc = getState() + acquires;
            setState(nextc);
            return true;
        }
        return false;
    }
}

Fair characteristics: lock() does not barge, directly enters acquire queue. tryAcquire calls hasQueuedPredecessors() to check queue; if someone waiting, does not grab, queues instead.

Pros: FIFO, no starvation.

Cons: lower throughput (all threads queue, even new threads arriving just as lock is released).

hasQueuedPredecessors() logic: queue has nodes besides head, and the first node's thread is not current thread → returns true (someone ahead in queue).

5. Shared Lock: How Does Semaphore Queue?

AQS supports two modes: exclusive ( EXCLUSIVE, e.g., ReentrantLock) and shared ( SHARED, e.g., Semaphore, CountDownLatch).

5.1 Shared Acquire: acquireShared

public final void acquireShared(int arg) {
    // tryAcquireShared return value:
    // >0: success, permits remain
    // =0: success, no permits remain
    // <0: failure
    if (tryAcquireShared(arg) < 0)
        doAcquireShared(arg); // failed, enqueue in shared queue
}

5.2 Shared Wait: doAcquireShared

private void doAcquireShared(int arg) {
    // 1. Enqueue shared-mode node (mode = SHARED)
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();
            if (p == head) {
                int r = tryAcquireShared(arg); // try acquire shared lock
                if (r >= 0) {
                    // ✅ Success, set head and propagate wakeup to successors
                    setHeadAndPropagate(node, r);
                    p.next = null;
                    if (interrupted)
                        selfInterrupt();
                    failed = false;
                    return;
                }
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

Differences from exclusive mode:

Node mode is SHARED (not EXCLUSIVE).

On success, calls setHeadAndPropagate(node, r) instead of setHead(node) — adds Propagation (wakeup propagation) .

5.3 Propagation Wakeup: setHeadAndPropagate

private void setHeadAndPropagate(Node node, int propagate) {
    Node h = head;
    setHead(node); // become new head

    // propagate > 0 means permits remain, or head status is PROPAGATE/SIGNAL
    // then continue waking successors (because shared mode: one permit can be shared by multiple threads)
    if (propagate > 0 || h == null || h.waitStatus < 0 ||
        (h = head) == null || h.waitStatus < 0) {
        Node s = node.next;
        if (s == null || s.isShared()) // successor is shared mode
            doReleaseShared(); // wake successor
    }
}

Shared mode core is propagation wakeup :

Exclusive: lock release wakes only one successor (lock held by single thread).

Shared: after a thread acquires shared lock, if permits remain ( propagate > 0), it continues waking successors so they can also attempt acquisition — this is "propagation".

Example: Semaphore with 5 permits, 3 threads waiting. First thread acquires 2 (3 left), wakes second; second acquires, wakes third; continues until permits exhausted.

5.4 Shared Release: releaseShared

public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) { // subclass implements, e.g., Semaphore releases permit
        doReleaseShared(); // wake successors
        return true;
    }
    return false;
}

private void doReleaseShared() {
    for (;;) {
        Node h = head;
        if (h != null && h != tail) {
            int ws = h.waitStatus;
            if (ws == Node.SIGNAL) {
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue; // CAS failed, retry
                unparkSuccessor(h); // wake successor
            }
            else if (ws == 0 &&
                !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue; // set PROPAGATE to ensure propagation continues
            if (h == head) // head unchanged, exit loop
                break;
        }
    }
}
PROPAGATE

state (-3) is unique to shared mode, ensuring wakeup propagation doesn't stop even if head changes during propagation.

6. Condition Variables: await/signal Full Flow

AQS's ConditionObject implements condition wait/notify, more flexible than Object.wait / notify (one lock can have multiple Conditions).

6.1 Wait: await

public final void await() throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();

    // 1. Add current thread to condition queue
    Node node = addConditionWaiter();

    // 2. Release lock (save state before release, re-acquire on wakeup)
    int savedState = fullyRelease(node);

    int interruptMode = 0;
    // 3. Spin: while still in condition queue, park
    while (!isOnSyncQueue(node)) {
        LockSupport.park(this); // suspend
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            break; // interrupted, exit
    }

    // 4. After signal, node moved to sync queue, re-acquire lock
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;

    // 5. Clean up cancelled nodes in condition queue
    if (node.nextWaiter != null)
        unlinkCancelledWaiters();

    // 6. Handle interrupt
    if (interruptMode != 0)
        reportInterruptAfterWait(interruptMode);
}
await

core steps: addConditionWaiter(): wrap thread as Node ( waitStatus=CONDITION), append to condition queue tail. fullyRelease(node): release lock (calls release(savedState)), because waiting cannot hold lock. while (!isOnSyncQueue(node)): while node still in condition queue, park suspend.

On signal, node moved to sync queue, isOnSyncQueue returns true, exit loop. acquireQueued(node, savedState): re-acquire lock in sync queue (same queuing as normal acquire).

After acquiring lock, await returns, continue business logic.

6.2 Enqueue Condition: addConditionWaiter

private Node addConditionWaiter() {
    Node t = lastWaiter;
    // If lastWaiter cancelled, clean up
    if (t != null && t.waitStatus != Node.CONDITION) {
        unlinkCancelledWaiters();
        t = lastWaiter;
    }
    // Create node, waitStatus=CONDITION
    Node node = new Node(Thread.currentThread(), Node.CONDITION);
    if (t == null)
        firstWaiter = node;
    else
        t.nextWaiter = node; // condition queue singly-linked via nextWaiter
    lastWaiter = node;
    return node;
}

6.3 Signal: signal

public final void signal() {
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException(); // must hold lock to signal

    Node first = firstWaiter;
    if (first != null)
        doSignal(first); // wake first node in condition queue
}

private void doSignal(Node first) {
    do {
        // Remove head from condition queue
        if ((firstWaiter = first.nextWaiter) == null)
            lastWaiter = null;
        first.nextWaiter = null;

        // transferForSignal: move node from condition queue to sync queue
    } while (!transferForSignal(first) && (first = firstWaiter) != null);
}

6.4 Transfer Node: transferForSignal

final boolean transferForSignal(Node node) {
    // 1. CAS waitStatus from CONDITION to 0 (prepare for sync queue)
    if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
        return false; // node cancelled

    // 2. Enqueue node at sync queue tail (same as addWaiter)
    Node p = enq(node);
    int ws = p.waitStatus;

    // 3. Set predecessor to SIGNAL so predecessor will unpark this node on release
    if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
        LockSupport.unpark(node.thread); // if predecessor cancelled, unpark directly

    return true;
}
signal

core:

Remove head node from condition queue.

CAS its waitStatus from CONDITION to 0.

Call enq(node) to append to sync queue tail.

Set predecessor to SIGNAL so lock release will unpark it.

Note: signal does NOT immediately grant lock; it only moves thread from condition queue to sync queue, where it waits for lock release then competes.

6.5 Condition Variable Complete Flow Diagram

Thread A (holds lock) calls condition.await()
    │
    ▼
addConditionWaiter() → enter condition queue (waitStatus=CONDITION)
    │
    ▼
fullyRelease() → release lock (state restored)
    │
    ▼
while (!isOnSyncQueue(node)) → still in condition queue
    │
    ▼
LockSupport.park() → suspend Thread A


Thread B (holds lock) calls condition.signal()
    │
    ▼
doSignal(first) → take condition queue head (Thread A)
    │
    ▼
transferForSignal(node)
    ├── CAS waitStatus from CONDITION to 0
    ├── enq(node) → append to sync queue tail
    └── set predecessor to SIGNAL
    │
    ▼
Thread B releases lock (unlock)
    │
    ▼
unparkSuccessor(head) → unpark sync queue first (Thread A)
    │
    ▼
Thread A woken, isOnSyncQueue returns true, exit while
    │
    ▼
acquireQueued(node, savedState) → re-acquire lock in sync queue
    │
    ▼
Got lock, await() returns, continue business code

Summary

AQS (AbstractQueuedSynchronizer) is the cornerstone of JUC concurrency tools. Its core consists of three things: state (synchronization state), CLH queue (wait queue), ConditionObject (condition variables) .

Key Points Recap

state : volatile int, modified via CAS, meaning defined by subclass (reentry count/permits/count), sole basis for AQS lock state decisions.

CLH Queue : doubly-linked list, head is virtual sentinel, tail is queue end; failed threads become Nodes, enqueue, park suspend; on release, unpark wakes head's successor. waitStatus states: SIGNAL(-1) (successor needs wakeup), CANCELLED(1) (cancelled), CONDITION(-2) (in condition queue), PROPAGATE(-3) (shared propagation), 0 (initial).

Exclusive Lock Flow : acquiretryAcquire (subclass) → fail → addWaiter enqueue → acquireQueued spin → predecessor is head retry → fail → park. releasetryRelease (subclass) → unparkSuccessor wake successor.

Fair vs Non-Fair :

Non-fair: lock() CAS barging, tryAcquire ignores queue, high throughput but possible starvation.

Fair: lock() queues directly, tryAcquire checks hasQueuedPredecessors, FIFO no starvation but lower throughput.

Shared Lock Flow : acquireSharedtryAcquireShared (return >=0 success) → fail → doAcquireShared enqueue.

On success, setHeadAndPropagate, if permits remain, propagate wakeup to successors. releaseSharedtryReleaseShareddoReleaseShared wake successors, PROPAGATE ensures continuous propagation.

Condition Variable Flow : await: enter condition queue → release lock → park suspend. signal: take condition queue head → move to sync queue → set predecessor SIGNAL.

After wakeup, re-acquire lock in sync queue, await returns.

AQS design embodies concurrency essence: use state for decisions, queue for ordering, park/unpark for suspend/resume, template method for extensibility . It avoids a single heavyweight lock managing all threads, instead using lock-free CAS + lightweight queue for efficient thread scheduling.

Understanding AQS means understanding JUC tool design philosophy and gaining core ability to diagnose concurrency issues.

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.

Condition VariableSemaphoreAQSReentrantLockJava ConcurrencyThread SynchronizationLock ImplementationCLH Queue
Java Tech Workshop
Written by

Java Tech Workshop

Focused on Java backend technologies, sharing fundamentals, multithreading, JVM, the Spring ecosystem, microservices, distributed systems, high concurrency, source‑code analysis, and practical experience. Continuously delivers high‑quality original content, interview guides, and learning roadmaps to help Java developers progress from beginner to advanced, enhancing technical skills and core competitiveness.

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.