When a lock looks simple, the interview gets tricky: ReentrantLock gives you the same safety as synchronized, but with more control.
Question: What is ReentrantLock in Java?
Answer: ReentrantLock is a mutual-exclusion lock, or mutex, that lets only one thread at a time enter a critical section. It is called reentrant because the same thread can acquire it more than once without deadlocking itself. You must release it the same number of times, usually in a finally block.
Interview-Ready Answer: “ReentrantLock is a thread lock from java.util.concurrent.locks that works like synchronized, but with extra control. The same thread can lock it repeatedly, and each acquisition increases an internal hold count that must be unlocked the same number of times. I use it when I need things like tryLock(), timeout, interruptible waiting, or a fair queue.”
ReentrantLock lives in java.util.concurrent.locks. Think of it as a mutex with extra controls: it protects a shared invariant, and it can be entered again by the same owner thread.
lock(), it first tries a fast path: if the lock is free, it atomically claims it.unlock(), the hold count decreases. When it reaches zero, ownership is cleared and one waiting thread is unparked.unlock(), Java throws IllegalMonitorStateException.tryLock() to avoid waiting forever.lockInterruptibly() so a request can be cancelled while waiting.Condition objects, such as notEmpty and notFull.| Feature | ReentrantLock | synchronized |
|---|---|---|
| Interruptible wait | Yes | No |
| Timeout | Yes | No |
| Fairness option | Yes | No |
| Multiple conditions | Yes | One wait set |
| Automatic release | No | Yes |
| Reentrant | Yes | Yes |
In the uncontended case, lock and unlock are O(1). The real cost is contention: blocked threads may park and unpark, which can cost microseconds to milliseconds depending on CPU load and scheduling. Fair locks usually reduce throughput because they limit barging and increase context switches. The default constructor is non-fair (new ReentrantLock()), which is usually the best throughput choice.
Space use is small for the lock object itself, but each blocked thread adds queue bookkeeping and the thread's own stack, which is often around 1 MB by default on many HotSpot setups, though it is configurable.
tryLock() can fail even if the lock becomes free a moment later.tryLock() on a fair lock can still barge; it does not fully obey fairness rules.unlock() must be in finally, or an exception can leak the lock.isHeldByCurrentThread() and getHoldCount() help with debugging nested locking.Real-World Example: In a checkout service, several request threads update an in-memory inventory cache. One method reserves stock, another applies a promo, and both must protect the same state. A ReentrantLock makes the critical section explicit, and tryLock(50, TimeUnit.MILLISECONDS) can fail fast if the system is overloaded instead of piling up requests.
What goes wrong when people misunderstand it: a developer acquires the lock, hits an exception, and forgets the finally release. After that, every checkout thread waits behind the stuck owner. The user symptom is a spinning payment page; the logs show threads blocked on ReentrantLock, the p99 latency climbs, and a thread dump reveals many threads in WAITING state. The outage is not a crash; it is a traffic jam caused by one unreleased lock.
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
public class Main {
static class Counter {
private final ReentrantLock lock;
private int value;
Counter(boolean fair) {
this.lock = new ReentrantLock(fair);
}
boolean isFair() {
return lock.isFair();
}
int getValue() {
lock.lock();
try {
return value;
} finally {
lock.unlock();
}
}
void reentrantOperation() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + " outer acquired, holdCount=" + lock.getHoldCount());
nestedOperation();
System.out.println(Thread.currentThread().getName() + " after nested, holdCount=" + lock.getHoldCount());
} finally {
lock.unlock();
}
}
private void nestedOperation() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + " inner acquired, holdCount=" + lock.getHoldCount());
value += 10;
} finally {
lock.unlock();
}
}
boolean tryIncrement(long timeoutMs) throws InterruptedException {
if (lock.tryLock(timeoutMs, TimeUnit.MILLISECONDS)) {
try {
value++;
return true;
} finally {
lock.unlock();
}
}
return false;
}
void interruptibleWork() throws InterruptedException {
lock.lockInterruptibly();
try {
value++;
} finally {
lock.unlock();
}
}
void holdLock(long sleepMs) {
lock.lock();
try {
sleep(sleepMs);
} finally {
lock.unlock();
}
}
private static void sleep(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
public static void main(String[] args) throws Exception {
Counter counter = new Counter(false);
System.out.println("Default fairness = " + counter.isFair());
counter.reentrantOperation();
System.out.println("Value after reentrant operation = " + counter.getValue());
Thread holder = new Thread(() -> counter.holdLock(600), "holder");
holder.start();
Thread.sleep(50);
Thread timedWorker = new Thread(() -> {
try {
boolean acquired = counter.tryIncrement(100);
System.out.println(Thread.currentThread().getName() + " tryLock result = " + acquired);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.out.println(Thread.currentThread().getName() + " was interrupted");
}
}, "timed-worker");
timedWorker.start();
Thread interruptibleWorker = new Thread(() -> {
try {
System.out.println(Thread.currentThread().getName() + " waiting interruptibly...");
counter.interruptibleWork();
System.out.println(Thread.currentThread().getName() + " acquired the lock");
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + " interrupted while waiting");
}
}, "interruptible-worker");
interruptibleWorker.start();
Thread.sleep(100);
interruptibleWorker.interrupt();
holder.join();
timedWorker.join();
interruptibleWorker.join();
System.out.println("Final value = " + counter.getValue());
ReentrantLock rogueLock = new ReentrantLock();
try {
rogueLock.unlock();
} catch (IllegalMonitorStateException e) {
System.out.println("Unlock without owning the lock throws " + e.getClass().getSimpleName());
}
}
}Follow-up & Tricky Questions:
lock() and lockInterruptibly()? lock() waits until the lock is available and does not respond to interrupts while waiting. lockInterruptibly() can stop waiting and throw InterruptedException.Condition instead of wait/notify? A Condition gives you a separate wait queue per reason, such as notEmpty and notFull. That makes the code easier to reason about than one shared monitor wait set.IllegalMonitorStateException, which is why unlock must happen in the same thread that acquired it.tryLock() respect fairness? No. Even on a fair lock, tryLock() can barge in if the lock happens to be free at that instant.Common Mistakes:
unlock() in finally — correction: always release the lock in a finally block so exceptions cannot leak it.Memory Hook: Think of ReentrantLock as a door with a stamp. The same person can come back in if they already have the stamp, but every entry must be matched with one exit.
Cheat Sheet:
tryLock() for fast failure.lockInterruptibly() for cancellable waits.Condition for multiple wait states.Practice Tasks:
List with a ReentrantLock and protect add/remove operations.tryLock timeout so your method returns a friendly message instead of blocking forever.