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.
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
- Simple synchronization — one synchronized is enough, code is simpler and more readable
- No contention — synchronized is optimized by the JVM (thin lock, elision)
- 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
- Always use try/finally:
lock.lock(); try { // code } finally { lock.unlock(); // MANDATORY otherwise permanent lock } -
Prefer synchronized — if you don’t need advanced ReentrantLock features
-
Avoid fairness — unless strictly necessary (performance!)
-
Use tryLock(timeout) — to avoid deadlock
-
Multiple Conditions — for granular wait/notify management
- 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