Fundamentals 17 min read

How ReentrantLock Works Under the Hood: A Deep Dive into Java’s AQS

This article breaks down the inner workings of Java’s ReentrantLock, explaining spin locks, the role of AbstractQueuedSynchronizer, fair vs. non‑fair locking, and step‑by‑step thread execution with code examples and diagrams to illustrate the complete lock acquisition and release process.

Code Ape Tech Column
Code Ape Tech Column
Code Ape Tech Column
How ReentrantLock Works Under the Hood: A Deep Dive into Java’s AQS

1. Spin lock concept

Spin refers to a thread repeatedly checking a condition until it can proceed. Using two threads competing for a resource, the first thread acquires the lock by setting state = 1. The second thread continuously checks state in a loop (spins) until the first thread releases the lock and resets state = 0.

volatile int state = 0; // atomic state variable
void lock() {
    while (!compareAndSet(0, 1)) {
        // spin
    }
    // critical section
}
void unlock() {
    state = 0;
}
boolean compareAndSet(int expect, int newValue) {
    // CAS operation, returns true on success
}

The CAS operation succeeds only when state is 0, turning it to 1 and granting the lock. If state is already 1, the thread keeps spinning, which wastes CPU cycles.

NOTE: Pure spin‑waiting is CPU‑intensive and therefore impractical for real‑world locks.

To improve efficiency, a sleep/wake mechanism is added to the basic spin algorithm.

volatile int state = 0;
Queue parkQueue;
void lock() {
    while (!compareAndSet(0, 1)) {
        park(); // suspend thread and add to wait queue
    }
    // critical section
    unlock();
}
void unlock() {
    lock_notify();
}
void park() {
    parkQueue.add(currentThread);
    releaseCpu(); // yield CPU
}
void lock_notify() {
    Thread t = parkQueue.header();
    unpark(t); // wake up next thread
}

2. Introducing ReentrantLock

Before JDK 1.6, synchronization relied on the synchronized keyword, which invokes OS‑level primitives and incurs higher overhead (a “heavyweight” lock). Doug Lea created ReentrantLock to perform locking at the JVM level, offering comparable or better performance and additional features such as interruptible lock acquisition.

3. ReentrantLock locking analysis

3.1 AQS overview

ReentrantLock is built on AbstractQueuedSynchronizer (AQS), a framework that manages a FIFO wait queue of nodes representing threads.

volatile Node prev; // previous node
volatile Node next; // next node
volatile Thread thread; // thread stored in the node

AQS also maintains head, tail, and a state field that records lock status.

private transient volatile Node head; // queue head
private transient volatile Node tail; // queue tail
private volatile int state; // 0 = unlocked, 1 = locked (re‑entry adds)
private transient Thread exclusiveOwnerThread; // owning thread
AQS queue diagram
AQS queue diagram

3.2 Overall locking flow

A demonstration program creates two threads ( t1 and t2) that each call lock.lock(), execute lockTest(), and then lock.unlock().

public class TestReentrantLock {
    public static void main(String[] args) {
        final ReentrantLock lock = new ReentrantLock(true);
        Thread t1 = new Thread("t1") {
            @Override public void run() {
                lock.lock();
                lockTest();
                lock.unlock();
            }
        };
        Thread t2 = new Thread("t2") {
            @Override public void run() {
                lock.lock();
                lockTest();
                lock.unlock();
            }
        };
        t1.start();
        t2.start();
    }
    public static void lockTest() {
        System.out.println(Thread.currentThread().getName());
        try { Thread.sleep(2000); System.out.println(" -------end"); }
        catch (InterruptedException e) { e.printStackTrace(); }
    }
}
Calling lock.lock() delegates to the internal Sync.lock() method provided by AQS.

Fair and non‑fair lock implementations differ only in the initial check performed by lock():

// Fair lock
final void lock() { acquire(1); }
// Non‑fair lock
final void lock() {
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);
}

The non‑fair version attempts a fast CAS first; if it fails, it falls back to the full acquire path.

3.3 Thread 1 execution (fair lock)

Thread 1 invokes acquire(1), which calls tryAcquire(1). Because state == 0 and the queue is empty ( hasQueuedPredecessors() returns false), the CAS succeeds, the thread becomes the exclusive owner, and the method returns.

public final void acquire(int arg) {
    if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}
acquire(int arg) obtains the lock exclusively; if it cannot, the thread is placed into the wait queue.
protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    } else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0) throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

Since the queue is empty, hasQueuedPredecessors() returns false, the CAS succeeds, and Thread 1 acquires the lock without any waiting.

Thread 1 flow diagram
Thread 1 flow diagram

3.4 Thread 2 execution (fair lock)

When Thread 2 attempts to lock, state == 1 and the current owner is Thread 1, so tryAcquire(1) returns false. The thread is then added to the queue via addWaiter(Node.EXCLUSIVE) and subsequently parked.

private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);
    Node pred = tail;
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    enq(node);
    return node;
}
private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        if (t == null) {
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

Images illustrate the two‑step queue construction (first a dummy node, then the real thread node).

First loop: create empty node
First loop: create empty node
Second loop: add thread node
Second loop: add thread node
final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed) cancelAcquire(node);
    }
}

If the predecessor is the head, the thread attempts to acquire the lock again; otherwise it parks (sleeps) until it is unparked by the releasing thread.

Thread 2 flow diagram
Thread 2 flow diagram

3.5 Contention scenarios

The article distinguishes three typical cases:

Only one thread: direct CAS, no queue (JVM level).

Threads alternate: direct CAS, no queue (JVM level).

High contention: threads are queued, and the lock acquisition may involve a spin followed by park() (OS level) depending on the intensity of competition.

When contention is intense, the lock invokes park() to suspend the thread; when contention is mild, it may spin once more before parking.

References:

[1] https://blog.csdn.net/java_lyvee/article/details/98966684

[2] 方腾飞. 《Java并发编程的艺术》

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.

JavaconcurrencyLockAQSReentrantLockthread synchronization
Code Ape Tech Column
Written by

Code Ape Tech Column

Former Ant Group P8 engineer, pure technologist, sharing full‑stack Java, job interview and career advice through a column. Site: java-family.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.