AQS

At the heart of the java.util.concurrent package lies a sophisticated and powerful framework known as AbstractQueuedSynchronizer (AQS). If you have ever used a ReentrantLock, a Semaphore, or a CountDownLatch, you have already interacted with the capabilities provided by AQS. It serves as the foundational infrastructure for building various synchronizers, abstracting away the grueling complexities of low-level thread management.

In the realm of concurrent programming, developers face several recurring challenges: how to manage a shared resource's state atomically, how to efficiently block threads when a resource is unavailable, and how to wake them up in an orderly fashion. AQS solves these problems by providing a reusable framework that handles the "heavy lifting" of queuing and thread scheduling, allowing developers to focus solely on the logic of the synchronization itself.

The Three Pillars of AQS

To master AQS, one must understand the three core components that drive its internal mechanism:

1. The Synchronization State (state)

The central piece of AQS is a single volatile int variable called state. This variable represents the current status of the synchronizer. Because it is marked as volatile, it ensures memory visibility across different threads.

AQS relies heavily on CAS (Compare-And-Swap) operations to modify this state. CAS allows for atomic updates without the need for heavy-weight locking, which is essential for high-performance concurrency. The interpretation of this state varies depending on the implementation:

  • In a ReentrantLock, the state represents the hold count (how many times the current thread has acquired the lock).
  • In a CountDownLatch, the state represents the remaining count of the countdown.

2. The CLH Wait Queue

When a thread attempts to acquire a resource and fails, it cannot simply "busy-wait" (spin) indefinitely, as this would waste CPU cycles. Instead, AQS utilizes a CLH (Craig, Landin, and Hagersten) variant queue.

This is a FIFO (First-In-First-Out) doubly linked queue of nodes. When a thread fails to acquire the state, AQS encapsulates the thread and its metadata into a Node object and places it into this queue. The queue manages the parking (blocking) and unparking (waking up) of threads, ensuring that waiting threads are processed in an orderly manner.

3. Acquisition and Release Modes

AQS is designed to be versatile, supporting two distinct modes of resource management:

  • Exclusive Mode: Only one thread can hold the resource at any given time (e.g., ReentrantLock).
  • Shared Mode: Multiple threads can hold the resource simultaneously (e.g., Semaphore or ReadWriteLock's read lock).

The Template Method Design Pattern

AQS is a masterclass in the Template Method Design Pattern. The framework provides a complete, high-level workflow for acquiring and releasing resources through public methods like acquire() and release(). However, the specific logic of how a resource is actually claimed or relinquished is left to the subclasses.

To create a custom synchronizer, a developer only needs to extend AQS and override a few protected "hook" methods:

  • tryAcquire(int): Attempts to acquire the state in exclusive mode. Returns true if successful.
  • tryRelease(int): Attempts to release the state in exclusive mode.
  • tryAcquireShared(int): Attempts to acquire the state in shared mode.
  • tryReleaseShared(int): Attempts to release the state in shared mode.
  • isHeldExclusively(): Returns true if the current thread holds the exclusive lock.

By implementing these methods, the developer defines the "business logic" of the lock, while AQS handles the complex orchestration of the queue and thread signaling.

Practical Implementation: Building a Custom Mutex

To illustrate the elegance of this design, let's implement a simple, non-reentrant Mutex (Mutual Exclusion lock) by extending AQS.

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;

/**
 * A simple non-reentrant Mutex implemented using AQS.
 */
public class Mutex implements Lock {

    // The internal synchronization logic encapsulated in a static inner class
    private static class Sync extends AbstractQueuedSynchronizer {

        @Override
        protected boolean isHeldExclusively() {
            // The lock is held if the state is 1
            return getState() == 1;
        }

        @Override
        protected boolean tryAcquire(int arg) {
            // Attempt to atomically transition state from 0 to 1
            if (compareAndSetState(0, 1)) {
                // Set the current thread as the owner of the lock
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }

        @Override
        protected boolean tryRelease(int arg) {
            if (getState() == 0) {
                throw new IllegalMonitorStateException();
            }
            // Clear the owner and reset state to 0
            setExclusiveOwnerThread(null);
            setState(0);
            return true;
        }

        Condition newCondition() {
            return new ConditionObject();
        }
    }

    private final Sync sync = new Sync();

    @Override
    public void lock() {
        sync.acquire(1);
    }

    @Override
    public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);
    }

    @Override
    public boolean tryLock() {
        return sync.tryAcquire(1);
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return sync.tryAcquireNanos(1, unit.toNanos(time));
    }

    @Override
    public void unlock() {
        sync.release(1);
    }

    @Override
    public Condition newCondition() {
        return sync.newCondition();
    }
}

In this example, notice how the Mutex class itself contains almost no synchronization logic. It simply delegates all calls to the Sync inner class. The Sync class, in turn, only cares about the state (0 for unlocked, 1 for locked). It doesn't need to know how to queue a thread or how to wake up a waiting thread—AQS handles all of that automatically.

Conclusion

AQS is a cornerstone of modern Java concurrency. By combining a volatile state, a FIFO CLH queue, and the Template Method pattern, it provides a robust and highly efficient framework for synchronization. Understanding AQS not only demystifies the behavior of standard Java locks but also empowers developers to build custom, high-performance synchronization primitives tailored to specific architectural needs.