Backend Development 10 min read

Understanding ReentrantLock: Fair and Non-Fair Lock Mechanisms in Java

This article explains the internal workings of Java's ReentrantLock, comparing fair and non‑fair lock acquisition, detailing the role of AQS, CLH queues, CAS operations, thread parking, and unlock procedures with full source‑code excerpts.

Top Architect
Top Architect
Top Architect
Understanding ReentrantLock: Fair and Non-Fair Lock Mechanisms in Java

ReentrantLock, like synchronized , provides thread synchronization but adds advanced features such as fairness, timeout, and interruptibility, and is built on the AbstractQueuedSynchronizer (AQS) framework.

Fair lock acquisition first checks the queue with hasQueuedPredecessors() before attempting to acquire the lock. The core lock() method is:

final void lock() {
    acquire(1);
}

The acquire(int) method tries tryAcquire and, if it fails, enqueues the thread and enters a spin‑wait loop:

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

The fair tryAcquire implementation checks for waiting predecessors and uses CAS to set the state:

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;
}

If acquisition fails, the thread is added to the CLH queue via addWaiter and possibly enq :

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;
            }
        }
    }
}

The queued thread then repeatedly calls acquireQueued to spin, check its predecessor, and possibly park:

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;
                failed = false;
                return interrupted;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

The helper shouldParkAfterFailedAcquire decides whether the thread can safely park based on the predecessor's wait status:

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL)
        return true;
    if (ws > 0) {
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

Parking itself is performed by:

private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);
    return Thread.interrupted();
}

Non‑fair lock acquisition differs only in that it attempts a CAS lock immediately, without checking the queue first:

final void lock() {
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);
}

The subsequent nonfairTryAcquire logic mirrors the fair version but omits the hasQueuedPredecessors check.

Unlocking simply releases the state and wakes the next waiting thread:

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

public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

In summary, the article details how fair and non‑fair ReentrantLock implementations share the same queue‑based spin‑and‑park mechanism after the initial acquisition attempt, highlighting the role of CAS, CLH queue management, and thread parking in achieving re‑entrancy and fairness.

JavaConcurrencyReentrantLockFairLockNonFairLockThreadSynchronization
Top Architect
Written by

Top Architect

Top Architect focuses on sharing practical architecture knowledge, covering enterprise, system, website, large‑scale distributed, and high‑availability architectures, plus architecture adjustments using internet technologies. We welcome idea‑driven, sharing‑oriented architects to exchange and learn together.

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.