Backend Development 20 min read

Demystifying Java Locks: From Pessimistic vs Optimistic to ReentrantLock Internals

This article explains the classification of Java locks—including pessimistic, optimistic, fair, and unfair variants—details their state transitions from biased to heavyweight, and walks through the source code of ReentrantLock and Condition to illustrate how synchronization, queuing, and thread parking are implemented in the JVM.

Architect's Guide
Architect's Guide
Architect's Guide
Demystifying Java Locks: From Pessimistic vs Optimistic to ReentrantLock Internals

Lock Classification

In Java, most locks are pessimistic; synchronized (including biased, lightweight, and heavyweight) and the

Lock

implementations provided by the JDK all follow this model. Optimistic locking is not a lock at all—it is a CAS‑based algorithm typically implemented with atomic classes from

java.util.concurrent.atomic

.

Pessimistic vs Optimistic Locks

Pessimistic locks assume contention and block threads, while optimistic locks rely on repeated CAS attempts and only retry when a conflict is detected.

Fair and Unfair Locks

A fair lock grants access to the longest‑waiting thread when the lock is released; an unfair lock may allow a later thread to acquire the lock first, leading to random or priority‑based ordering.

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

The code above shows how a non‑fair lock first tries to acquire the lock via CAS; if it fails, it falls back to the AQS

acquire

method, which may enqueue the thread.

Biased → Lightweight → Heavyweight

When a synchronized block is entered for the first time, the lock object becomes a biased lock, favoring the first thread that acquires it. If a second thread contends, the lock upgrades to a lightweight (spin) lock, where threads repeatedly attempt CAS on the object header. Prolonged spinning leads to a heavyweight lock, which puts contending threads to sleep instead of busy‑waiting.

Exclusive vs Shared Locks

Exclusive locks allow only the head thread to hold the lock; subsequent threads are queued. Shared locks permit multiple readers to hold the lock simultaneously, but exclusive mode is used for writes.

How to Implement Synchronization

Four common strategies are presented:

Pure spin‑lock (CPU intensive).

Spin‑lock with

Thread.yield()

to give up the CPU when acquisition fails.

Spin‑lock combined with

Thread.sleep()

(requires careful sleep duration selection).

Spin‑lock combined with

LockSupport.park()

(used by

ReentrantLock

).

volatile int status = 0;
Queue parkQueue;

void lock() {
    while (!compareAndSet(0, 1)) {
        park();
    }
    // lock acquired
}

void unlock() {
    lock_notify();
}

void park() {
    parkQueue.add(currentThread);
    releaseCpu();
}

void lock_notify() {
    Thread t = parkQueue.header();
    unpark(t);
}

ReentrantLock Source Analysis

ReentrantLock uses the fourth strategy (park + spin). The following snippet shows a simple runnable that repeatedly acquires and releases a fair

ReentrantLock

:

public class MyRunnable implements Runnable {
    private int num = 0;
    private ReentrantLock lock = new ReentrantLock(true);
    @Override
    public void run() {
        while (num < 20) {
            lock.lock();
            try {
                num++;
                Log.e("zzf", Thread.currentThread().getName() + " got lock, num is " + num);
            } finally {
                lock.unlock();
            }
        }
    }
}

The constructors of

ReentrantLock

create either a non‑fair (

NonfairSync

) or a fair (

FairSync

) synchronizer. In the example,

new ReentrantLock(true)

selects the fair implementation.

public ReentrantLock() {
    sync = new NonfairSync();
}

public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

Calling

lock()

delegates to

sync.lock()

, which invokes

acquire(1)

. The

acquire

method first attempts

tryAcquire

; if it fails, the thread is enqueued via

addWaiter(Node.EXCLUSIVE)

and then waits in the AQS queue.

protected final boolean tryAcquire(int var1) {
    Thread current = Thread.currentThread();
    int state = getState();
    if (state == 0) {
        if (!hasQueuedPredecessors() && compareAndSetState(0, var1)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    } else if (current == getExclusiveOwnerThread()) {
        int next = state + var1;
        if (next < 0) throw new Error("Maximum lock count exceeded");
        setState(next);
        return true;
    }
    return false;
}

The internal

Node

class defines the queue nodes and several status constants (CANCELLED, SIGNAL, CONDITION, PROPAGATE). The AQS queue is a FIFO linked list of these nodes.

static final class Node {
    static final Node SHARED = new Node(); // shared mode
    static final Node EXCLUSIVE = null;    // exclusive mode
    static final int CANCELLED = 1;
    static final int SIGNAL = -1;
    static final int CONDITION = -2;
    static final int PROPAGATE = -3;
    volatile int waitStatus;
    volatile Node prev;
    volatile Node next;
    volatile Thread thread;
    Node nextWaiter;
}

The method

hasQueuedPredecessors()

determines whether the current thread must wait in the queue.

public final boolean hasQueuedPredecessors() {
    Node t = tail;
    Node h = head;
    Node s;
    return h != t && ((s = h.next) == null || s.thread != Thread.currentThread());
}

When a thread cannot acquire the lock immediately, it is added to the queue via

addWaiter

and then waits in

acquireQueued

, which may park the thread using

LockSupport.park()

until it becomes the head of the queue.

private Node addWaiter(Node mode) {
    Node node = new Node(mode);
    for (;;) {
        Node oldTail = tail;
        if (oldTail != null) {
            node.prev = oldTail;
            if (compareAndSetTail(oldTail, node)) {
                oldTail.next = node;
                return node;
            }
        } else {
            initializeSyncQueue();
        }
    }
}

Condition Support

Beyond the main synchronization queue,

ConditionObject

maintains a separate condition queue. Threads that call

await()

are moved from the sync queue to the condition queue, release the lock, and park until signalled.

public final void await() throws InterruptedException {
    if (Thread.interrupted()) throw new InterruptedException();
    Node node = addConditionWaiter();
    int savedState = fullyRelease(node);
    int interruptMode = 0;
    while (!isOnSyncQueue(node)) {
        LockSupport.park(this);
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0) break;
    }
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;
    if (node.nextWaiter != null) unlinkCancelledWaiters();
    if (interruptMode != 0) reportInterruptAfterWait(interruptMode);
}

The

signal()

method transfers the first waiting node from the condition queue back to the sync queue, where it can contend for the lock again.

public final void signal() {
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    Node first = firstWaiter;
    if (first != null)
        doSignal(first);
}

private void doSignal(Node first) {
    do {
        if ((firstWaiter = first.nextWaiter) == null)
            lastWaiter = null;
        first.nextWaiter = null;
    } while (!transferForSignal(first) && (first = firstWaiter) != null);
}

Illustration

Lock state diagram
Lock state diagram

The diagram above visualizes the relationship between the synchronization queue and the condition queue during lock acquisition, release, and signalling.

JavaConcurrencyMultithreadingLocksAQSReentrantLockCondition
Architect's Guide
Written by

Architect's Guide

Dedicated to sharing programmer-architect skills—Java backend, system, microservice, and distributed architectures—to help you become a senior architect.

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.