Question 7 · Section 9

What is reentrant lock?

A Reentrant Lock is a mechanism that allows a thread that already owns a lock to re-acquire it without self-deadlocking.

Language versions: English Russian Ukrainian

Junior Level

Basic Understanding

A Reentrant Lock is a mechanism that allows a thread that already owns a lock to re-acquire it without self-deadlocking.

Why it matters: without reentrancy, a recursive call to a synchronized method would lead to deadlock — the thread would wait for itself. With reentrancy, the JVM counts how many times the thread acquired the lock (a counter) and only releases when the counter = 0.

Simple Analogy

Imagine a door with a code lock:

  • Regular lock: even if you’re already inside, to re-enter — you need to wait
  • Reentrant lock: if you’re already inside, you can exit and re-enter as many times as you want

Example in Java

synchronized (reentrant by default)

public class ReentrantDemo {
    public synchronized void methodA() {
        System.out.println("Method A");
        methodB(); // Calls another synchronized method — does NOT block!
    }

    public synchronized void methodB() {
        System.out.println("Method B");
    }
}

ReentrantLock

import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockDemo {
    private final ReentrantLock lock = new ReentrantLock();

    public void methodA() {
        lock.lock();
        try {
            System.out.println("Method A");
            methodB(); // Same thread can acquire the lock again
        } finally {
            lock.unlock();
        }
    }

    public void methodB() {
        lock.lock();
        try {
            System.out.println("Method B");
        } finally {
            lock.unlock();
        }
    }
}

How the Reentrancy Counter Works

Thread 1: lock.lock()    → counter = 1, enters
Thread 1: lock.lock()    → counter = 2, enters (same thread!)
Thread 1: lock.unlock()  → counter = 1, still owner
Thread 1: lock.unlock()  → counter = 0, releases
Thread 2: lock.lock()    → counter = 1, now Thread 2 is owner

When to use ReentrantLock instead of synchronized

Situation synchronized ReentrantLock
Simple synchronization Yes Overkill
Wait timeout No Yes (tryLock(timeout))
Interruptible wait No Yes (lockInterruptibly())
Multiple conditions (Condition) No (only wait/notify) Yes (multiple Condition)
Fairness (fair queue) No Yes

Middle Level

Internal Structure (AQS)

ReentrantLock is built on AbstractQueuedSynchronizer (AQS) — a framework from java.util.concurrent:

AQS — an abstract class framework from java.util.concurrent, on which ALL Java concurrency utilities are built: ReentrantLock, Semaphore, CountDownLatch, ReentrantReadWriteLock. If you understand AQS — you understand all of Java concurrency.

// Simplified AQS structure
abstract class AbstractQueuedSynchronizer {
    private volatile int state;        // Lock counter (reentrancy)
    private transient Thread exclusiveOwnerThread; // Owner thread

    // Wait queue (doubly-linked list)
    static final class Node {
        volatile Thread thread;
        volatile int waitStatus;
        Node prev, next;
    }
    private transient volatile Node head;
    private transient volatile Node tail;
}

Acquisition Algorithm

// Pseudocode lock()
protected final boolean tryAcquire(int acquires) {
    Thread current = Thread.currentThread();
    int c = getState();

    if (c == 0) {
        // Lock is free — try to acquire
        if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    } else if (current == getExclusiveOwnerThread()) {
        // Same thread — reentrancy
        int nextc = c + acquires;
        if (nextc < 0) throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

Fairness

// Non-fair mode (default) — faster
ReentrantLock nonFair = new ReentrantLock(); // fair = false

// Non-fair (default) — allows "barge": if the lock was just released and a new thread tries to acquire it, it can get it immediately without entering the queue. This avoids context switch (park/wakeup) and gives higher throughput.

// Fair mode — strict FIFO
ReentrantLock fair = new ReentrantLock(true); // fair = true
Mode Behavior Performance
Non-fair (default) Thread can “barge” and acquire the lock even if there are threads in queue Higher throughput
Fair Threads get access strictly in queue order (FIFO) Lower throughput, no starvation

Advanced ReentrantLock Features

1. tryLock() — attempt without waiting

if (lock.tryLock()) {
    try {
        // Acquired — execute
    } finally {
        lock.unlock();
    }
} else {
    // Failed — do something else
    System.out.println("Lock is busy, skipping");
}

2. tryLock(timeout) — wait with timeout

if (lock.tryLock(100, TimeUnit.MILLISECONDS)) {
    try {
        // Execute
    } finally {
        lock.unlock();
    }
} else {
    // Timeout — failed to acquire within 100ms
}

3. lockInterruptibly() — interruptible wait

try {
    lock.lockInterruptibly();
    try {
        // Execute
    } finally {
        lock.unlock();
    }
} catch (InterruptedException e) {
    // Thread was interrupted while waiting
    Thread.currentThread().interrupt();
}

Condition — replacement for wait/notify

ReentrantLock lock = new ReentrantLock();
Condition notEmpty = lock.newCondition();
Condition notFull = lock.newCondition();

public void produce(int item) throws InterruptedException {
    lock.lock();
    try {
        while (queue.size() == MAX_SIZE) {
            // while (not ready) — REQUIRED! Spurious wakeups:
            // thread may wake up WITHOUT signal(). while re-checks the condition.
            // If you use if — the thread continues without a real signal.
            notFull.await(); // Wait for space
        }
        queue.add(item);
        notEmpty.signal(); // Notify consumers
    } finally {
        lock.unlock();
    }
}

public int consume() throws InterruptedException {
    lock.lock();
    try {
        while (queue.isEmpty()) {
            // while (not ready) — REQUIRED! Spurious wakeups:
            // thread may wake up WITHOUT signal(). while re-checks the condition.
            // If you use if — the thread continues without a real signal.
            notEmpty.await(); // Wait for data
        }
        int item = queue.remove(0);
        notFull.signal(); // Notify producers
        return item;
    } finally {
        lock.unlock();
    }
}

Advantage over wait/notify: you can have different Conditions for different events.


When ReentrantLock is NOT needed

  1. Simple synchronization — one synchronized is enough, code is simpler and more readable
  2. No contention — synchronized is optimized by the JVM (thin lock, elision)
  3. Don’t need Lock features — tryLock, fairness, Condition not required

Key difference: synchronized is a language keyword (JVM-level), ReentrantLock is a library class. This determines all other differences.


Senior Level

Under the Hood: NonfairSync and FairSync

ReentrantLock uses two internal classes:

public class ReentrantLock implements Lock {
    private final Sync sync;

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

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

NonfairSync

static final class NonfairSync extends Sync {
    protected final boolean tryAcquire(int acquires) {
        // Thread can acquire the lock even if others are in the queue
        // This is called "barge"

        // "barge" — a new thread attempts to acquire the lock without entering the wait queue. If lucky — the lock is free, the thread gets it instantly without parking.
        return nonfairTryAcquire(acquires);
    }
}

FairSync

static final class FairSync extends Sync {
    protected final boolean tryAcquire(int acquires) {
        // First check: are there waiters in the queue?
        if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(Thread.currentThread());
            return true;
        }
        return false;
    }
}

Performance: Fair vs Non-fair

Non-fair: throughput ~100K ops/sec
Fair:     throughput ~50K ops/sec  (~50% slower; approximate values, depend on CPU and JVM)

Why fair is slower:

  • Need to check the queue every time
  • Threads park/wakeup more often (context switching)
  • No “barge” optimization

When to use fair:

  • When starvation must be avoided
  • When processing order is critical

AQS: How the Wait Queue Works

                    ┌─────────────────────────────────────┐
                    │           AQS Queue                  │
                    └─────────────────────────────────────┘

    head ←→ [Node T1] ←→ [Node T2] ←→ [Node T3] ←→ tail
             (WAITING)    (WAITING)    (WAITING)

When T1 gets signal:
    head ←→ [Node T2] ←→ [Node T3] ←→ tail
             (ACTIVE)     (WAITING)

ReentrantLock vs synchronized in Java 8+

Characteristic synchronized ReentrantLock
Performance (uncontended) ~10-20 ns ~15-25 ns
Performance (contended) ~1000+ ns ~500-1000 ns
Reentrancy Yes (automatic) Yes (state counter)
Fairness No Yes (optional)
tryLock No Yes
lockInterruptibly No Yes
Multiple Conditions No (one wait set) Yes
Readability Higher Lower (needs try/finally)

Conclusion: In Java 8+, synchronized performance is heavily optimized. Use ReentrantLock only for advanced features (timeouts, fairness, multiple conditions).

Diagnostics

jstack

jstack <pid>
"worker-1" #10 RUNNABLE
  locked <0x000000076af0c8d0> (a java.util.concurrent.locks.ReentrantLock$NonfairSync)

"worker-2" #11 WAITING
  parking to wait for  <0x000000076af0c8d0> (a java.util.concurrent.locks.ReentrantLock$NonfairSync)

Programmatic Diagnostics

ReentrantLock lock = new ReentrantLock();

// Check: how many times the current thread acquired the lock
int holdCount = lock.getHoldCount();

// Number of threads in queue
int queueLength = lock.getQueueLength();

// Any waiters?
boolean hasQueued = lock.hasQueuedThreads();

// Which thread owns it?
Thread owner = lock.getOwner();

Best Practices

  1. Always use try/finally:
    lock.lock();
    try {
        // code
    } finally {
        lock.unlock(); // MANDATORY otherwise permanent lock
    }
    
  2. Prefer synchronized — if you don’t need advanced ReentrantLock features

  3. Avoid fairness — unless strictly necessary (performance!)

  4. Use tryLock(timeout) — to avoid deadlock

  5. Multiple Conditions — for granular wait/notify management

  6. Monitor queueLength — growing queue = contention problem

Interview Cheat Sheet

Must know:

  • Reentrancy = a thread owning the lock can re-acquire it without deadlock (counter increment/decrement)
  • synchronized is reentrant automatically; ReentrantLock — a library alternative built on AQS
  • AQS (AbstractQueuedSynchronizer): volatile state (counter) + wait queue (doubly-linked list)
  • Non-fair (default) allows “barge” — jump the queue; Fair — strict FIFO
  • Fair mode ~50% slower than non-fair due to queue checks and frequent context switches
  • ReentrantLock provides: tryLock(), tryLock(timeout), lockInterruptibly(), multiple Conditions
  • Condition — replacement for wait/notify with the advantage: you can have multiple conditions (notEmpty, notFull)

Frequent follow-up questions:

  • Why is non-fair faster than fair? — A new thread can acquire a free lock without parking, avoiding context switch
  • What is “barge”? — When a thread attempts to acquire the lock without entering the queue; if lucky — gets it instantly
  • Why is try/finally needed with ReentrantLock? — If an exception occurs before unlock(), the lock stays acquired forever → deadlock
  • When to choose fair mode? — When processing order matters and starvation must be avoided (rarely)

Red flags (do NOT say):

  • “ReentrantLock is always faster than synchronized” — no, in Java 8+ synchronized is heavily optimized
  • “Fairness is always needed for correctness” — no, only when starvation is critical
  • “Condition is the same as wait/notify” — no, Condition allows multiple wait sets
  • “AQS is just a counter” — no, it’s a full framework with a wait queue and CAS operations

Related topics:

  • [[4. What is monitor in Java]] — built-in monitor vs ReentrantLock
  • [[5. How does synchronized work at monitor level]] — lock escalation vs AQS
  • [[8. What are Atomic classes]] — AQS uses CAS to acquire state
  • [[9. What is CAS (Compare-And-Swap)]] — CAS is the foundation of AQS