ReentrantLock, Semaphore, and CountDownLatch look unrelated at the API level, but each holds a private Sync extends AbstractQueuedSynchronizer. The Sync subclass overrides tryAcquire / tryRelease and decides what state represents. AbstractQueuedSynchronizer (AQS) is the base class: it owns state, the CLH sync queue, acquire / release, and the nested ConditionObject.


1. Architecture

1.1 Design overview

A blocking synchronizer has two operations: acquire waits until state permits progress; release updates state and may wake waiters. In the JDK this splits across two classes:

ClassResponsibility
Sync extends AQSOverride tryAcquire* / tryRelease*; define what state means
AbstractQueuedSynchronizerHold state, run the acquire / release loop, manage the CLH queue, park threads via LockSupport

AbstractQueuedSynchronizer: synchronizers, Sync subclass, and AQS internals

The diagram shows the call chain:

  1. ReentrantLock, Semaphore, etc. — public API (lock, acquire, await).
  2. Sync — inner class that extends AQS and implements the try* hooks. ReentrantLock.Sync treats state as hold count; Semaphore.Sync treats it as permit count.
  3. AbstractQueuedSynchronizer — when tryAcquire fails, AQS enqueues the thread on a CLH FIFO queue and blocks it with LockSupport.park. On release, AQS calls tryRelease then signalNext to unpark a successor. Also contains ConditionObject for await / signal, and extends AbstractOwnableSynchronizer to track the exclusive owner thread.

AbstractQueuedLongSynchronizer is the same design with a long state field — used by ReentrantReadWriteLock.

Both modes share one sync queue; the node type records the mode:

ModeAQS APINode classExamples
Exclusiveacquire / releaseExclusiveNodeReentrantLock, write lock
SharedacquireShared / releaseSharedSharedNodeSemaphore, CountDownLatch.await, read lock

CAS, memory fences, and the OS primitives behind park are covered in Synchronization.

1.2 AbstractQueuedSynchronizer

AQS turns a single int state and a FIFO queue of waiting threads into a blocking synchronizer. The subclass decides whether a thread may pass by implementing the try* hooks; AQS decides what happens when it may not: it enqueues the thread, parks it, wakes it on release, and removes it on interrupt or timeout.

@startuml
abstract class AbstractOwnableSynchronizer {
    - transient Thread exclusiveOwnerThread
}

abstract class AbstractQueuedSynchronizer {
    - transient volatile Node head
    - transient volatile Node tail
    - volatile int state
    + acquire(int arg) : void
    + acquireInterruptibly(int arg) : void
    + tryAcquireNanos(int arg, long nanosTimeout) : boolean
    + release(int arg) : boolean
    + acquireShared(int arg) : void
    + acquireSharedInterruptibly(int arg) : void
    + releaseShared(int arg) : boolean
    ~ acquire(Node node, int arg, boolean shared, boolean interruptible, boolean timed, long time) : int
    - cancelAcquire(Node node, boolean interrupted, boolean interruptible) : int
    - cleanQueue() : void
    - {static} signalNext(Node h) : void
    - {static} signalNextIfShared(Node h) : void
    # tryAcquire(int arg) : boolean
    # tryRelease(int arg) : boolean
    # tryAcquireShared(int arg) : int
    # tryReleaseShared(int arg) : boolean
}

abstract class Node {
    ~ volatile Node prev
    ~ volatile Node next
    ~ Thread waiter
    ~ volatile int status
}

class ExclusiveNode
class SharedNode
class ConditionNode {
    ~ ConditionNode nextWaiter
}

AbstractOwnableSynchronizer <|-- AbstractQueuedSynchronizer
Node <|-- ExclusiveNode
Node <|-- SharedNode
Node <|-- ConditionNode
AbstractQueuedSynchronizer "1" o-- "*" Node : sync queue
@enduml

State and hooks

state is accessed only through getState, setState and compareAndSetState. The subclass gives it a meaning and overrides the hooks of the mode it supports; the default implementations throw UnsupportedOperationException.

// AbstractQueuedSynchronizer.tryAcquire
// exclusive: true if the caller now owns the synchronizer
protected boolean tryAcquire(int arg) {
    throw new UnsupportedOperationException();
}

// AbstractQueuedSynchronizer.tryRelease
// exclusive: true if fully released, so a waiter may now acquire
protected boolean tryRelease(int arg) {
    throw new UnsupportedOperationException();
}

// AbstractQueuedSynchronizer.tryAcquireShared
// shared: < 0 fail; 0 success, nothing left for others; > 0 success, others may succeed too
protected int tryAcquireShared(int arg) {
    throw new UnsupportedOperationException();
}

// AbstractQueuedSynchronizer.tryReleaseShared
// shared: true if a waiting acquire may now succeed
protected boolean tryReleaseShared(int arg) {
    throw new UnsupportedOperationException();
}

Sync queue and node status

The sync queue is a doubly linked list of nodes, created on first contention. head is a dummy node or the node of the last thread that acquired; the first waiter is head.next, and it is the only queued thread that calls the hook. New waiters are appended at tail by CAS.

// AbstractQueuedSynchronizer constants
static final int WAITING   = 1;          // must be 1           -- node will park; releaser must unpark it
static final int CANCELLED = 0x80000000; // must be negative    -- node gave up; status < 0 tests it
static final int COND      = 2;          // in a condition wait -- node is on a condition queue (section 1.3)

Acquire

The public methods try the hook once and fall back to the internal loop only when it fails. They differ only in the flags they pass:

// AbstractQueuedSynchronizer.acquire
// ReentrantLock.lock: exclusive, ignore interrupts, no timeout
public final void acquire(int arg) {
    if (!tryAcquire(arg))                                // fast path: no queueing
        acquire(null, arg, false, false, false, 0L);     // shared=false, interruptible=false, timed=false
}

// AbstractQueuedSynchronizer.acquireInterruptibly
// ReentrantLock.lockInterruptibly
public final void acquireInterruptibly(int arg)
    throws InterruptedException {
    if (Thread.interrupted() ||                          // already interrupted: fail before trying
        (!tryAcquire(arg) && acquire(null, arg, false, true, false, 0L) < 0))  // < 0: interrupted while queued
        throw new InterruptedException();
}

// AbstractQueuedSynchronizer.tryAcquireNanos
// ReentrantLock.tryLock(long, TimeUnit)
public final boolean tryAcquireNanos(int arg, long nanosTimeout)
    throws InterruptedException {
    if (!Thread.interrupted()) {
        if (tryAcquire(arg))
            return true;
        if (nanosTimeout <= 0L)
            return false;
        int stat = acquire(null, arg, false, true, true,
                           System.nanoTime() + nanosTimeout);  // deadline, not duration
        if (stat > 0)                                    // acquired
            return true;
        if (stat == 0)                                   // timed out
            return false;
    }
    throw new InterruptedException();                    // stat < 0: interrupted
}

// AbstractQueuedSynchronizer.acquireShared
// Semaphore.acquireUninterruptibly; the shared variants of the two methods above
// back Semaphore.acquire, CountDownLatch.await and Semaphore.tryAcquire(long, TimeUnit)
public final void acquireShared(int arg) {
    if (tryAcquireShared(arg) < 0)
        acquire(null, arg, true, false, false, 0L);      // shared=true
}

The internal loop does one step per iteration and then starts over, so the hook is retried between enqueueing, announcing WAITING and parking:

// AbstractQueuedSynchronizer.acquire
// returns > 0 acquired, 0 timed out, < 0 interrupted (interruptible only)
final int acquire(Node node, int arg, boolean shared,
                  boolean interruptible, boolean timed, long time) {
    Thread current = Thread.currentThread();
    byte spins = 0, postSpins = 0;   // retries upon unpark of first thread
    boolean interrupted = false, first = false;
    Node pred = null;               // predecessor of node when enqueued

    for (;;) {
        // 1. Am I the first waiter (pred == head)? If not, make sure pred is alive.
        if (!first && (pred = (node == null) ? null : node.prev) != null &&
            !(first = (head == pred))) {
            if (pred.status < 0) {
                cleanQueue();           // predecessor cancelled
                continue;
            } else if (pred.prev == null) {
                Thread.onSpinWait();    // ensure serialization
                continue;
            }
        }
        // 2. Only the first waiter, or a thread not yet queued, calls the hook.
        if (first || pred == null) {
            boolean acquired;
            try {
                if (shared)
                    acquired = (tryAcquireShared(arg) >= 0);
                else
                    acquired = tryAcquire(arg);
            } catch (Throwable ex) {
                cancelAcquire(node, interrupted, false);   // hook threw: leave the queue
                throw ex;
            }
            if (acquired) {
                if (first) {
                    // 3. Dequeue myself: my node becomes the new dummy head.
                    node.prev = null;
                    head = node;
                    pred.next = null;
                    node.waiter = null;
                    if (shared)
                        signalNextIfShared(node);   // shared: pass the wake-up on
                    if (interrupted)
                        current.interrupt();        // restore the interrupt cleared while waiting
                }
                return 1;
            }
        }
        Node t;
        // 4. Lazy setup: create the queue, then my node, then link it at tail.
        if ((t = tail) == null) {           // initialize queue
            if (tryInitializeHead() == null)
                return acquireOnOOME(shared, arg);
        } else if (node == null) {          // allocate; retry before enqueue
            try {
                node = (shared) ? new SharedNode() : new ExclusiveNode();
            } catch (OutOfMemoryError oome) {
                return acquireOnOOME(shared, arg);
            }
        } else if (pred == null) {          // try to enqueue
            node.waiter = current;
            node.setPrevRelaxed(t);         // avoid unnecessary fence
            if (!casTail(t, node))
                node.setPrevRelaxed(null);  // back out
            else
                t.next = node;
        // 5. Woken first waiter spins briefly (1, 3, 7, ... times) before parking again.
        } else if (first && spins != 0) {
            --spins;                        // reduce unfairness on rewaits
            Thread.onSpinWait();
        // 6. Announce WAITING, then loop once more: a release that happens now
        //    sees WAITING and unparks me, or my next hook attempt sees it.
        } else if (node.status == 0) {
            node.status = WAITING;          // enable signal and recheck
        // 7. Park until unpark, interrupt, timeout or spurious wake-up.
        } else {
            spins = postSpins = (byte)((postSpins << 1) | 1);
            try {
                long nanos;
                if (!timed)
                    LockSupport.park(this);
                else if ((nanos = time - System.nanoTime()) > 0L)
                    LockSupport.parkNanos(this, nanos);
                else
                    break;                  // deadline passed: cancel
            } catch (Error | RuntimeException ex) {
                cancelAcquire(node, interrupted, interruptible); // cancel & rethrow
                throw ex;
            }
            node.clearStatus();
            // park does not say why it returned; remember (and clear) any interrupt
            if ((interrupted |= Thread.interrupted()) && interruptible)
                break;                      // interruptible: cancel
        }
    }
    return cancelAcquire(node, interrupted, interruptible);
}

Release

Release runs the hook and, if it allows progress, wakes the first waiter. It dequeues nothing; the woken thread dequeues itself when its hook succeeds (step 3 in the acquire loop above).

// AbstractQueuedSynchronizer.release
public final boolean release(int arg) {
    if (tryRelease(arg)) {          // 1. hook: e.g. hold count reached 0
        signalNext(head);           // 2. wake the first waiter; it dequeues itself
        return true;
    }
    return false;
}

// AbstractQueuedSynchronizer.releaseShared
public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {    // 1. hook: e.g. permits returned, latch reached 0
        signalNext(head);           // 2. wake the first waiter; it propagates to shared successors
        return true;
    }
    return false;
}

// AbstractQueuedSynchronizer.signalNext
private static void signalNext(Node h) {
    Node s;
    // 1. Find the first waiter. status == 0 means it has not announced WAITING,
    //    so it has not parked and will see the released state on its next hook attempt.
    if (h != null && (s = h.next) != null && s.status != 0) {
        s.getAndUnsetStatus(WAITING);   // 2. clear WAITING
        LockSupport.unpark(s.waiter);   // 3. wake it; it resumes at step 1 of acquire
    }
}

Exclusive and shared mode

Both modes use the same queue and the same acquire loop. The snippet lists all seven steps of the loop; the shared flag changes steps 2, 3 and 4:

// AbstractQueuedSynchronizer.acquire (all steps; real code where the mode matters)
for (;;) {
    // 1. Am I the first waiter? Clean up if the predecessor was cancelled.
    ...                                        // same for both modes

    // 2. First waiter (or not yet queued) calls the hook of its mode.
    if (first || pred == null) {
        boolean acquired;
        if (shared)
            acquired = (tryAcquireShared(arg) >= 0);   // >= 0 acquired; > 0 others may acquire too
        else
            acquired = tryAcquire(arg);                 // true: I am the only holder
        if (acquired) {
            if (first) {
                // 3. Dequeue myself: my node becomes the new head.
                node.prev = null;
                head = node;
                pred.next = null;
                node.waiter = null;
                if (shared)
                    signalNextIfShared(node);   // shared: wake the next shared waiter
                                                // exclusive: wake nobody
                ...
            }
            return 1;
        }
    }
    // 4. Lazy setup: queue, then a node of my mode, then link it at tail.
    ...
    } else if (node == null) {
        node = (shared) ? new SharedNode() : new ExclusiveNode();   // the node records the mode
    } else if (pred == null) {
        ...                                    // CAS onto tail
    // 5. Woken first waiter spins briefly.
    } else if (first && spins != 0) {
        ...
    // 6. Announce WAITING.
    } else if (node.status == 0) {
        ...
    // 7. Park; on wake-up go back to step 1.
    } else {
        ...                                    // same for both modes
    }
}

In exclusive mode one release lets exactly one waiter through. In shared mode several threads may hold at once, so the waiter that just acquired wakes its successor if that successor is also shared:

// AbstractQueuedSynchronizer.signalNextIfShared
private static void signalNextIfShared(Node h) {
    Node s;
    // 1. Find the successor; only a SharedNode qualifies. A queued writer
    //    (ExclusiveNode) stops the chain.
    if (h != null && (s = h.next) != null &&
        (s instanceof SharedNode) && s.status != 0) {
        s.getAndUnsetStatus(WAITING);   // 2. clear WAITING
        LockSupport.unpark(s.waiter);   // 3. wake it; it tries its own hook and may pass the wake-up on
    }
}

A release wakes one thread, so shared mode relies on this propagation. When CountDownLatch.countDown brings the count to zero, each await caller that succeeds wakes the next, until every waiter has passed or a hook fails.

Interrupts

An interrupt makes park return just like unpark does; it does not grant the synchronizer. The steps below show where an interrupt is checked, recorded, and acted on:

// AbstractQueuedSynchronizer.acquireInterruptibly (before the loop)
if (Thread.interrupted() ||                    // 0. already interrupted: fail without trying
    (!tryAcquire(arg) && acquire(null, arg, false, true, false, 0L) < 0))
    throw new InterruptedException();          // 8. loop returned CANCELLED

// AbstractQueuedSynchronizer.acquire (all steps; real code where interrupts matter)
for (;;) {
    // 1. Am I the first waiter? Clean up if the predecessor was cancelled.
    ...
    // 2. First waiter (or not yet queued) calls the hook.
    if (first || pred == null) {
        ...
        if (acquired) {
            if (first) {
                // 3. Dequeue myself; an uninterruptible caller gets its interrupt back.
                ...
                if (interrupted)
                    current.interrupt();       // lock(): restore the flag cleared in step 7
            }
            return 1;
        }
    }
    // 4. Lazy setup: queue, node, link at tail.
    ...
    // 5. Woken first waiter spins briefly.
    ...
    // 6. Announce WAITING.
    ...
    // 7. Park. An interrupt makes park return just like unpark does.
    } else {
        ...
        LockSupport.park(this);
        ...
        node.clearStatus();
        if ((interrupted |= Thread.interrupted()) && interruptible)  // read and clear the flag
            break;                             // interruptible: leave the loop
                                               // lock(): fall through, back to step 1
    }
}
return cancelAcquire(node, interrupted, interruptible);   // 8. CANCELLED (< 0) -> InterruptedException

Because lock() restores the flag after acquiring, a caller can still react to an interrupt that arrived while it waited:

lock.lock();                                      // never throws InterruptedException
try {
    if (Thread.currentThread().isInterrupted())   // interrupt received while waiting
        return;
    // critical section
} finally {
    lock.unlock();
}

Cancellation

A waiter leaves the queue through cancelAcquire when it is interrupted on an interruptible path, times out, or the hook throws:

// AbstractQueuedSynchronizer.cancelAcquire
private int cancelAcquire(Node node, boolean interrupted,
                          boolean interruptible) {
    if (node != null) {
        node.waiter = null;         // 1. nobody should unpark me any more
        node.status = CANCELLED;    // 2. mark only; other threads may be linking to it
        if (node.prev != null)
            cleanQueue();           // 3. unlink cancelled nodes, wake a new first waiter
    }
    if (interrupted) {              // 4. report the outcome
        if (interruptible)
            return CANCELLED;       //    negative -> InterruptedException
        else
            Thread.currentThread().interrupt();   // keep the flag for the caller
    }
    return 0;                       //    timeout -> tryAcquireNanos returns false
}

cleanQueue walks from tail toward head and unsplices cancelled nodes by CAS:

// AbstractQueuedSynchronizer.cleanQueue
private void cleanQueue() {
    for (;;) {                               // restart point
        for (Node q = tail, s = null, p, n;;) { // (p, q, s) triples
            // 1. Walk from tail toward head: q is the current node, p its predecessor, s its successor.
            if (q == null || (p = q.prev) == null)
                return;                      // end of list
            // 2. Links changed under me: restart from tail.
            if (s == null ? tail != q : (s.prev != q || s.status < 0))
                break;                       // inconsistent
            // 3. q is cancelled: unlink it by pointing s (or tail) at p.
            if (q.status < 0) {              // cancelled
                if ((s == null ? casTail(q, p) : s.casPrev(q, p)) &&
                    q.prev == p) {
                    if (s != null)
                        p.casNext(q, s);     // OK if fails
                    if (p.prev == null)      // p is head: s is now the first waiter;
                        signalNext(p);       // wake it in case the release went to q
                }
                break;
            }
            // 4. p.next is stale: repair it so signalNext can find q.
            if ((n = p.next) != q) {         // help finish
                if (n != null && q.prev == p) {
                    p.casNext(n, q);
                    if (p.prev == null)
                        signalNext(p);
                }
                break;
            }
            // 5. Move one node toward head.
            s = q;
            q = q.prev;
        }
    }
}

Waiters also clean up for themselves: at the start of each acquire iteration (step 1 in the loop above), a waiter whose predecessor has status < 0 calls cleanQueue.

1.3 ConditionObject

ConditionObject is nested inside AQS and backs Lock.newCondition(). It reuses ConditionNode but maintains a separate condition queue (firstWaiter / lastWaiter), distinct from the CLH sync queue:

@startuml
interface Condition {
    + await()
    + awaitUninterruptibly()
    + awaitNanos(long nanosTimeout)
    + await(long time, TimeUnit unit)
    + awaitUntil(Date deadline)
    + signal()
    + signalAll()
}

abstract class Node {
    volatile Node prev
    volatile Node next
    Thread waiter
    volatile int status
}

class ConditionObject implements Condition {
    transient ConditionNode firstWaiter
    transient ConditionNode lastWaiter
}

interface ManagedBlocker {
    + block() : boolean
    + isReleasable() : boolean
}

class ConditionNode extends Node implements ManagedBlocker {
    ConditionNode nextWaiter
}

ConditionObject *-right- ConditionNode : condition queue
@enduml

await() (caller must hold the exclusive lock):

  1. enableWait(node) — enqueue on the condition list, then release(savedState).
  2. Park (ForkJoinPool.managedBlock or LockSupport.park) until signalled.
  3. reacquire(node, savedState) — competitive acquire; may re-enter the sync queue.
  4. Restore interrupt status if interrupted after a real signal.

signal() transfers nodes from the condition queue to the sync queue. The signalled thread does not run until it re-acquires the lock:

private void doSignal(ConditionNode first, boolean all) {
    while (first != null) {
        ConditionNode next = first.nextWaiter;
        if ((first.getAndUnsetStatus(COND) & COND) != 0) {
            enqueue(first);
            if (!all) break;
        }
        first = next;
    }
}

Unlike synchronized, each Condition instance has its own wait set rather than sharing one per monitor.

Interrupts and cancellation in condition waits

Condition handling reuses Node.status but applies COND rather than CANCELLED for the primary wait protocol. A ConditionNode on the condition queue carries COND | WAITING; transfer to the sync queue clears COND and proceeds through normal acquire.

enableWait failure. If the lock is not held exclusively, or release fails, the node is marked CANCELLED and IllegalMonitorStateException is thrown — the thread never entered a valid wait:

node.status = CANCELLED; // lock not held or inconsistent
throw new IllegalMonitorStateException();

Interrupt or timeout while still on the condition queue. In await(), awaitNanos(), and related timed methods, the wait loop tests whether the node remains on the condition queue by atomically clearing and inspecting the COND bit:

if (interrupted |= Thread.interrupted()) {
    if (cancelled = (node.getAndUnsetStatus(COND) & COND) != 0)
        break;              // still on condition queue — abort wait
}
// else: interrupted after signal() already cleared COND and enqueued node
cancelledMeaningSubsequent action
trueCOND was present — thread not yet transferred by signal()unlinkCancelledWaiters(node); reacquire; throw InterruptedException or return false on timeout
falsesignal() already ran — doSignal cleared COND and called enqueuereacquire via sync queue; restore interrupt if interrupted after signal

doSignal skips nodes whose COND bit was already cleared (for example, by a concurrent interrupt):

if ((first.getAndUnsetStatus(COND) & COND) != 0)
    enqueue(first);

unlinkCancelledWaiters. Removes condition-queue entries that no longer carry the COND flag — nodes aborted before transfer. The sync-queue CANCELLED marker is not used for this path; the node is simply unlinked from firstWaiter / lastWaiter.

reacquire. After any await variant, the thread re-enters the sync queue through non-interruptible acquire(node, savedState, …) — even following interruptible await(), and even when the wait was cancelled on the condition queue: the thread must hold the lock again before await throws InterruptedException, because the caller’s finally { lock.unlock(); } expects it to.

awaitUninterruptibly. Does not use the cancelled / COND abort path; the loop runs until canReacquire(node) — that is, until signal() has enqueued the node and links are consistent.


2. Implementation

2.1 LockSupport

AQS stores the waiting thread in Node.waiter and calls LockSupport.unpark from signalNext. Each thread holds at most one permit; park() consumes it or blocks. Parking is per-thread, not bound to a lock object. Short spins (postSpins, Thread.onSpinWait) before parking cut context switches when the releaser is still on-CPU.

2.2 Built-in synchronizers

ClassAQS basestate meaningMode
ReentrantLockAbstractQueuedSynchronizerhold count (0 = unlocked)exclusive
ReentrantReadWriteLockAbstractQueuedLongSynchronizerhigh 32 = reads, low 32 = writesboth
SemaphoreAbstractQueuedSynchronizeravailable permitsshared
CountDownLatchAbstractQueuedSynchronizercountdown (0 = open)shared
CyclicBarrier— (ReentrantLock + Condition)count under lock—
FutureTask— (own CAS state + WaitNode stack)task lifecycle—

ReentrantLock

A reentrant mutual-exclusion lock: one thread holds it at a time, but the owner may acquire it again. It supports optional FIFO fairness, timed and interruptible acquire, and multiple condition variables on the same lock. Use it wherever synchronized is too limited — tryLock, fairness, or separate notFull / notEmpty waits on one buffer.

@startuml
interface Lock {
    + lock()
    + lockInterruptibly()
    + tryLock()
    + tryLock(long time, TimeUnit unit)
    + unlock()
    + newCondition() : Condition
}

interface Condition {
    + await()
    + signal()
    + signalAll()
}

class ConditionObject implements Condition

class ReentrantLock implements Lock {
    final Sync sync
}

abstract class Sync extends AbstractQueuedSynchronizer {
    + newCondition() : ConditionObject
}

class NonfairSync extends Sync
class FairSync extends Sync

ReentrantLock *-- Sync
Sync ..> ConditionObject : newCondition()
@enduml

lock.newCondition() returns a ConditionObject on the same Sync. A thread **await**s only while holding the lock; await releases the lock and waits on the condition queue; signal moves waiters back to the sync queue. One lock can have many conditions (e.g. notFull and notEmpty).

Fairness is not in AQS — it is chosen in the Sync subclass. The default constructor uses NonfairSync; new ReentrantLock(true) uses FairSync:

public ReentrantLock() {
    sync = new NonfairSync();
}

public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

NonfairSync skips the queue check, so a new thread can barge ahead of waiters:

final boolean initialTryLock() {
    Thread current = Thread.currentThread();
    if (compareAndSetState(0, 1)) {
        setExclusiveOwnerThread(current);
        return true;
    } else if (getExclusiveOwnerThread() == current) {
        setState(getState() + 1);
        return true;
    }
    return false;
}

protected final boolean tryAcquire(int acquires) {
    if (getState() == 0 && compareAndSetState(0, acquires)) {
        setExclusiveOwnerThread(Thread.currentThread());
        return true;
    }
    return false;
}

FairSync calls hasQueuedThreads() / hasQueuedPredecessors() before CAS:

final boolean initialTryLock() {
    Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        if (!hasQueuedThreads() && compareAndSetState(0, 1)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    } else if (getExclusiveOwnerThread() == current) {
        setState(++c);
        return true;
    }
    return false;
}

protected final boolean tryAcquire(int acquires) {
    if (getState() == 0 && !hasQueuedPredecessors() &&
        compareAndSetState(0, acquires)) {
        setExclusiveOwnerThread(Thread.currentThread());
        return true;
    }
    return false;
}

lock() always tries initialTryLock() before acquire(1), so a fair lock still takes the fast path when the queue is empty.

state is the reentrant hold count; exclusiveOwnerThread is the owner. tryRelease decrements and returns true at zero, which triggers signalNext:

protected final boolean tryRelease(int releases) {
    int c = getState() - releases;
    if (getExclusiveOwnerThread() != Thread.currentThread())
        throw new IllegalMonitorStateException();
    boolean free = (c == 0);
    if (free) setExclusiveOwnerThread(null);
    setState(c);
    return free;
}

ReentrantReadWriteLock

A read/write lock: many threads may hold the read lock together; the write lock excludes all readers and other writers. Both modes are reentrant. It fits read-heavy data — caches, config snapshots — where writes are infrequent but must see a consistent view.

@startuml
top to bottom direction

class AbstractOwnableSynchronizer {
    exclusiveOwnerThread : Thread
    + getExclusiveOwnerThread() : Thread
    # setExclusiveOwnerThread(Thread)
}

abstract class AbstractQueuedLongSynchronizer {
    state : long
    head : Node
    tail : Node
    + acquire(long)
    + release(long) : boolean
    + acquireShared(long)
    + releaseShared(long) : boolean
    # compareAndSetState(long, long) : boolean
    # tryAcquire(long) : boolean
    # tryRelease(long) : boolean
    # tryAcquireShared(long) : long
    # tryReleaseShared(long) : boolean
}

abstract class Sync {
    {static} SHARED_SHIFT : int = 32
    {static} SHARED_UNIT : long
    {static} EXCLUSIVE_MASK : long
    firstReader : Thread
    firstReaderHoldCount : int
    readHolds : ThreadLocalHoldCounter
    cachedHoldCounter : HoldCounter
    {static} sharedCount(long) : int
    {static} exclusiveCount(long) : int
    # tryAcquire(long) : boolean
    # tryRelease(long) : boolean
    # tryAcquireShared(long) : long
    # tryReleaseShared(long) : boolean
    {abstract} readerShouldBlock() : boolean
    {abstract} writerShouldBlock() : boolean
}

class NonfairSync {
    writerShouldBlock() : boolean
    readerShouldBlock() : boolean
}

class FairSync {
    writerShouldBlock() : boolean
    readerShouldBlock() : boolean
}

class ReentrantReadWriteLock {
    readerLock : ReadLock
    writerLock : WriteLock
    sync : Sync
}

class ReadLock {
    sync : Sync
    + lock()
    + unlock()
}

class WriteLock {
    sync : Sync
    + lock()
    + unlock()
}

AbstractOwnableSynchronizer <|-- AbstractQueuedLongSynchronizer
AbstractQueuedLongSynchronizer <|-- Sync
Sync <|-- NonfairSync
Sync <|-- FairSync
ReentrantReadWriteLock *-- ReadLock
ReentrantReadWriteLock *-- WriteLock
ReentrantReadWriteLock *-- Sync
ReadLock --> Sync
WriteLock --> Sync
@enduml

AbstractQueuedLongSynchronizer supplies state, the sync queue, and acquire / release. Sync packs the read count into the high 32 bits of state and the write count into the low 32. tryAcquire and tryRelease implement the write lock; tryAcquireShared and tryReleaseShared implement the read lock. ReadLock.lock calls acquireShared; WriteLock.lock calls acquire.

NonfairSync lets a writer barge (writerShouldBlock returns false) and blocks a new reader only when the queue head appears to be a waiting writer. FairSync makes both readerShouldBlock and writerShouldBlock return hasQueuedPredecessors(). Per-thread read recursion is tracked by firstReader for the first reader and by ThreadLocalHoldCounter for the rest.

Semaphore

A counting semaphore tracks a pool of permits. acquire takes one and blocks when the pool is empty; release returns one. There is no owner — any thread may release. Typical uses are connection pools, rate limiters, and any cap of N concurrent workers.

@startuml
class Semaphore {
    final Sync sync
    + acquire()
    + release()
    + tryAcquire()
}

abstract class Sync extends AbstractQueuedSynchronizer {
    + nonfairTryAcquireShared(int) : int
}

class NonfairSync extends Sync
class FairSync extends Sync

Semaphore *-- Sync
@enduml

state holds available permits. NonfairSync subtracts via CAS with no queue check. FairSync returns -1 when hasQueuedPredecessors() is true, forcing the thread to enqueue:

// NonfairSync — barging allowed
protected int tryAcquireShared(int acquires) {
    for (;;) {
        int available = getState();
        int remaining = available - acquires;
        if (remaining < 0 || compareAndSetState(available, remaining))
            return remaining;
    }
}

// FairSync
protected int tryAcquireShared(int acquires) {
    for (;;) {
        if (hasQueuedPredecessors())
            return -1;
        int available = getState();
        int remaining = available - acquires;
        if (remaining < 0 || compareAndSetState(available, remaining))
            return remaining;
    }
}

CountDownLatch

A one-shot countdown latch starts at N. Threads await until countDown has been called N times; then every waiter is released at once. The count cannot be reset — use a new latch or a CyclicBarrier when the gate must fire again. Common pattern: wait for all workers to finish startup before processing begins.

@startuml
class CountDownLatch {
    final Sync sync
    + await()
    + countDown()
    + getCount() : int
}

class Sync extends AbstractQueuedSynchronizer {
    + getCount() : int
}

CountDownLatch *-- Sync
@enduml

state is the remaining count. await succeeds only when state == 0; the last countDown returns true from tryReleaseShared and wakes all waiters.

protected int tryAcquireShared(int acquires) {
    return (getState() == 0) ? 1 : -1;
}

protected boolean tryReleaseShared(int releases) {
    for (;;) {
        int c = getState();
        if (c == 0) return false;
        int nextc = c - 1;
        if (compareAndSetState(c, nextc))
            return nextc == 0;
    }
}

CyclicBarrier

A cyclic barrier synchronizes a fixed number of parties. Each calls await; when the last arrives, an optional action runs and everyone proceeds. The barrier then resets for the next round — suited to parallel algorithms that advance in lockstep phases. reset() or a broken barrier starts a fresh cycle.

@startuml
interface Condition {
    + await()
    + signalAll()
}

class ConditionObject implements Condition

class ReentrantLock {
    + newCondition() : Condition
}

class CyclicBarrier {
    ReentrantLock lock
    Condition trip
    int parties
    int count
    Generation generation
}

class Generation {
    boolean broken
}

CyclicBarrier *-- ReentrantLock
CyclicBarrier o-- Condition : trip
ReentrantLock ..> ConditionObject : newCondition()
note right of Condition
  trip: parties await here
  until count hits 0;
  last party signalAll()
  in nextGeneration()
end note
@enduml

Field trip is the condition all parties share. Under lock, each party decrements count; non-last threads trip.await(); the last thread runs the barrier action and trip.signalAll() in nextGeneration() to release everyone for the next cycle.

Fields and setup:

private final ReentrantLock lock = new ReentrantLock();
private final Condition trip = lock.newCondition();

Each party decrements count under lock. Non-last parties trip.await(); the last party runs barrierCommand, then nextGeneration() resets count and trip.signalAll() wakes everyone:

private void nextGeneration() {
    trip.signalAll();
    count = parties;
    generation = new Generation();
}

// inside dowait(), after lock.lock()
int index = --count;
if (index == 0) {          // last party — trip the barrier
    if (barrierCommand != null)
        barrierCommand.run();
    nextGeneration();
    return 0;
}
for (;;) {
    if (!timed)
        trip.await();      // wait for last party to signal
    else if (nanos > 0L)
        nanos = trip.awaitNanos(nanos);
    if (g != generation)     // woken by nextGeneration()
        return index;
}

FutureTask

A FutureTask wraps a Callable or Runnable, runs it on an executor, and exposes the result through Future. Callers block on get() until the task completes, fails, or is cancelled — the usual task type behind ExecutorService.submit.

@startuml
interface Future<V> {
    + get() : V
    + cancel(boolean) : boolean
}

interface Runnable
interface RunnableFuture<V> extends Runnable, Future

class FutureTask<V> implements RunnableFuture {
    volatile int state
    volatile WaitNode waiters
}

class WaitNode {
    Thread thread
    WaitNode next
}

FutureTask *-right- WaitNode
@enduml

Summary

PieceRole
state + hooksSubclass decides when acquire / release succeeds
acquire / releaseTemplate loop: try → enqueue → park → retry
CLH queueFIFO waiters; fair mode checks hasQueuedPredecessors
LockSupportPer-thread park / unpark
ConditionObjectSeparate condition queue; signal enqueues onto sync queue
Concrete syncsThin wrappers mapping domain semantics onto state

Every AQS-backed class wraps a Sync inner class: read how state is encoded, then read tryAcquire* / tryRelease*.