Backend Development 12 min read

Understanding Java's AbstractQueuedSynchronizer (AQS) and ReentrantLock Implementation

The article explains Java’s AbstractQueuedSynchronizer framework and how ReentrantLock uses its inner Sync classes—FairSync and NonfairSync—to manage lock acquisition via CAS, queueing, spinning, and parking, detailing the acquire/release loops, fair vs non‑fair behavior, and interview‑ready insights.

HelloTech
HelloTech
HelloTech
Understanding Java's AbstractQueuedSynchronizer (AQS) and ReentrantLock Implementation

During recent interview preparations, many candidates showed only superficial knowledge of Java locks and multithreading. This article provides a deeper, up‑to‑date explanation of Java's lock mechanisms, focusing on the AbstractQueuedSynchronizer (AQS) framework and the concrete implementation of ReentrantLock .

AQS (AbstractQueuedSynchronizer) is an abstract class that uses a FIFO queue to manage thread synchronization. It is the foundation for all Java synchronizers such as CountDownLatch , read‑write locks, and re‑entrant locks.

The typical usage of ReentrantLock is shown in the class comment (image omitted). Internally, ReentrantLock delegates most work to an inner Sync class, which has two subclasses: FairSync and NonfairSync . These subclasses override initialTryLock and tryAcquire to implement the differences between fair and non‑fair locking.

The lock() method ultimately calls the subclass's initialTryLock . In the default NonfairSync , initialTryLock attempts to acquire the lock via a CAS operation. If the CAS succeeds, the current thread becomes the owner; otherwise, the method checks whether the current thread already holds the lock (re‑entrancy) and increments the state counter accordingly. If acquisition fails, acquire(1) is invoked.

The core acquire method (shown below) is the most complex part of AQS. It handles shared vs exclusive modes, interruptibility, and timeout logic. The method repeatedly loops, checking the node's predecessor, cleaning cancelled nodes, and attempting to acquire the lock. When the node becomes the head of the queue, it tries tryAcquire (or tryAcquireShared ) and, on success, updates the queue head and possibly signals the next waiter.

for (;;) {
    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;
        }
    }
    if (first || pred == null) {
        boolean acquired;
        try {
            if (shared)
                acquired = (tryAcquireShared(arg) >= 0);
            else
                acquired = tryAcquire(arg);
        } catch (Throwable ex) {
            cancelAcquire(node, interrupted, false);
            throw ex;
        }
        if (acquired) {
            if (first) {
                node.prev = null;
                head = node;
                pred.next = null;
                node.waiter = null;
                if (shared) signalNextIfShared(node);
                if (interrupted) current.interrupt();
            }
            return 1;
        }
    }
    // enqueue or retry logic follows ...
}

The loop also contains the logic for node allocation, enqueuing the node at the tail, handling spin‑waits, setting the node status to WAITING , and performing timed or untimed parking via LockSupport.park / parkNanos . If the thread is interrupted and the operation is interruptible, the loop breaks and the acquisition is cancelled.

When the lock is released, release(1) decrements the state counter. If the state reaches zero, the owner thread reference is cleared, and the next waiting node is unparked using LockSupport.unpark . This wakes the head of the queue.

Fair vs. non‑fair locks differ mainly in the acquisition path: a fair lock always checks the queue first and enqueues if the queue is non‑empty, while a non‑fair lock attempts an immediate CAS acquisition before considering the queue. Consequently, non‑fair locks can acquire the lock without queuing, giving earlier threads a disadvantage.

In summary, the article walks through the essential parts of AQS: the volatile state variable, CAS‑based lock acquisition, a double‑linked queue of waiting nodes, parking of threads, and unparking on release. Understanding these mechanisms helps interviewees explain Java's lock behavior beyond memorized textbook answers.

Javabackend developmentconcurrencymultithreadingLockAQSReentrantLock
HelloTech
Written by

HelloTech

Official Hello technology account, sharing tech insights and developments.

0 followers
Reader feedback

How this landed with the community

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