Fundamentals 30 min read

Unlocking Java Concurrency: A Deep Dive into AQS, Locks, and Condition Queues

This article explains Java's AbstractQueuedSynchronizer (AQS) framework, covering its MESA monitor model, entry and condition wait queues, exclusive and shared lock acquisition and release, and how core concurrency utilities like ReentrantLock, ReadWriteLock, CountDownLatch, Semaphore, ThreadPoolExecutor, and CyclicBarrier are built upon it.

Su San Talks Tech
Su San Talks Tech
Su San Talks Tech
Unlocking Java Concurrency: A Deep Dive into AQS, Locks, and Condition Queues

Hello everyone, I am Su San.

In Java, AQS (AbstractQueuedSynchronizer) provides a FIFO queue‑based framework for implementing locks and related synchronizers such as semaphores and events.

AQS has two main responsibilities: manipulating the state variable and providing the queuing and blocking mechanisms.

1 Monitor Model

Java uses the MESA monitor model to protect class fields and methods, ensuring thread‑safety. The model defines a shared variable, a condition variable, and a condition‑wait queue.

MESA encapsulates the shared variable and its operations; a thread must acquire the lock before entering the monitor, otherwise it joins the entry‑wait queue.

Even after acquiring the lock, a thread may still wait in the condition‑wait queue if the condition is not satisfied.

When a thread is signalled from the condition‑wait queue, it must re‑enter the entry‑wait queue to try to obtain the lock again.

The AQS monitor model replaces the MESA model with a FIFO entry‑wait queue and a ConditionObject that can create multiple condition queues.

2 Entry‑Wait Queue

2.1 Acquire Exclusive Lock

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

Note: tryAcquire is abstract; concrete AQS subclasses implement it for their specific lock.

2.1.1 Enqueue

When lock acquisition fails, addWaiter creates a new node and appends it to the FIFO queue.

2.1.2 After Enqueue – Spinning to Acquire

The acquireQueued method repeatedly checks the node’s waitStatus and attempts to acquire the lock.

CANCELLED (1): node gave up.

SIGNAL (‑1): predecessor should wake this node.

CONDITION (‑2): used by ConditionObject.

PROPAGATE (‑3): shared mode propagation.

0: intermediate state.

2.1.3 Exclusive Lock with Interrupt

Method acquireInterruptibly(int arg) checks the thread’s interrupt status before trying to acquire the lock and also during the spin; if interrupted, it throws InterruptedException.

2.1.4 Exclusive Lock with Timeout

Method tryAcquireNanos(int arg, long nanosTimeout) adds timeout checks both during spinning and before parking the thread.

2.2 Release Exclusive Lock

Releasing consists of invoking the abstract tryRelease (implemented by subclasses) and then waking a successor node if the head is non‑null and its waitStatus is not zero.

2.3 Acquire Shared Lock

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

returns:

negative – acquisition failed.

zero – acquisition succeeded but further shared acquires will fail.

positive – acquisition succeeded and further shared acquires may also succeed.

2.3.1 doAcquireShared

If tryAcquireShared returns a positive value, the thread becomes the new head and propagates the wake‑up to successors.

2.3.2 Shared Lock with Interrupt

Method acquireSharedInterruptibly(int arg) checks interrupt status before and during the spin, throwing InterruptedException if interrupted.

2.3.3 Shared Lock with Interrupt and Timeout

Method tryAcquireSharedNanos(int arg, long nanosTimeout) adds timeout checks similar to the exclusive version.

2.4 Release Shared Lock

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

After a successful tryReleaseShared, doReleaseShared spins to unpark successors while the queue has at least two nodes.

2.5 Summary

AQS uses a FIFO queue to implement entry‑wait queues and a state variable to drive exclusive and shared lock behavior, forming the basis for many Java concurrency utilities.

3 Concurrency Locks Built on AQS

3.1 ReentrantLock

ReentrantLock’s internal Sync class extends AQS. It implements tryAcquire, tryRelease, and isHeldExclusively. The lock can be fair or non‑fair; fair locks always queue, while non‑fair locks try CAS first.

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

3.1.1 Fair vs Non‑Fair

Fair lock checks for a predecessor node before CAS; non‑fair lock attempts CAS immediately.

3.2 ReentrantReadWriteLock

Uses a single Sync that implements both exclusive and shared methods, allowing concurrent reads and exclusive writes.

static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }

3.2.1 Read Lock

ReadLock calls acquireShared(1). The sync checks that no exclusive lock is held and that the shared count is below the maximum before incrementing the shared portion of state.

3.2.2 Write Lock

WriteLock calls acquire(1). It succeeds if state is zero or if the current thread already holds the exclusive lock (re‑entrancy).

3.3 CountDownLatch

CountDownLatch’s internal Sync extends AQS with a shared state initialized to the count. await uses acquireSharedInterruptibly(1); countDown uses releaseShared(1).

public CountDownLatch(int count) {
    if (count < 0) throw new IllegalArgumentException("count < 0");
    this.sync = new Sync(count);
}

3.4 Semaphore

Semaphore also extends AQS; its state holds the number of permits. acquire uses acquireSharedInterruptibly(1), and release uses releaseShared(1). It can be fair or non‑fair.

3.5 ThreadPoolExecutor

ThreadPoolExecutor uses an internal Worker class that implements its own exclusive lock via AQS to protect state changes when interrupting workers.

4 Condition‑Variable Wait Queues

4.1 Official Example

public class BoundedBuffer {
    final Lock lock = new ReentrantLock();
    final Condition notFull  = lock.newCondition();
    final Condition notEmpty = lock.newCondition();
    final Object[] items = new Object[100];
    int putptr, takeptr, count;

    public void put(Object x) throws InterruptedException {
        lock.lock();
        try {
            while (count == items.length)
                notFull.await();
            items[putptr] = x;
            if (++putptr == items.length) putptr = 0;
            ++count;
            notEmpty.signal();
        } finally { lock.unlock(); }
    }

    public Object take() throws InterruptedException {
        lock.lock();
        try {
            while (count == 0)
                notEmpty.await();
            Object x = items[takeptr];
            if (++takeptr == items.length) takeptr = 0;
            --count;
            notFull.signal();
            return x;
        } finally { lock.unlock(); }
    }
}

The two conditions notFull and notEmpty represent the two wait queues used by the buffer.

4.2 Principle

Condition objects are built on top of AQS. They maintain a singly‑linked wait queue (different from the doubly‑linked entry‑wait queue). Nodes in the condition queue have waitStatus = -2.

4.3 await

Calling await performs three steps: (1) add the thread to the condition queue, (2) fully release the associated lock (setting state to 0), and (3) block the thread until it is signalled.

4.4 signal / signalAll

signal

moves the first waiting node whose waitStatus == -2 from the condition queue to the entry‑wait queue, where it will attempt to reacquire the lock. signalAll transfers all waiting nodes.

4.5 Usage Example – CyclicBarrier

public static void main(String[] args) {
    CyclicBarrier barrier = new CyclicBarrier(2, () -> {
        System.out.println("All parties arrived");
    });
    ExecutorService exec = Executors.newFixedThreadPool(2);
    exec.submit(() -> {
        try { System.out.println("Thread 1"); barrier.await(); }
        catch (Exception e) { e.printStackTrace(); }
    });
    exec.submit(() -> {
        try { System.out.println("Thread 2"); barrier.await(); }
        catch (Exception e) { e.printStackTrace(); }
    });
    exec.shutdown();
}

Each thread calls await on the barrier’s internal condition; when the last thread arrives, the barrier signals all waiting threads.

4.6 Summary

Java’s monitor model follows the MESA paradigm. AQS supplies a FIFO entry‑wait queue, a state variable for exclusive/shared lock semantics, and ConditionObject for condition‑wait queues. All major concurrency utilities in java.util.concurrent are built on this foundation.

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.

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