Explicit Locks — ReentrantLock, ReadWriteLock & StampedLock
The Lock interface, lock/unlock in try-finally, tryLock and lockInterruptibly, fairness, Condition variables, ReentrantReadWriteLock, StampedLock optimistic reads, and synchronized vs ReentrantLock.
#Introduction
synchronized is simple but inflexible: you can't time out while waiting, can't respond to interrupts while blocked, can't check whether a lock is free, and have only one wait set per lock. The java.util.concurrent.locks package (Java 5) adds explicit locks that solve all of these.
#The Lock Interface
| Method | Behaviour |
|---|---|
lock() | Acquire, waiting as long as necessary (not interruptible) |
lockInterruptibly() | Acquire, but abort with InterruptedException if interrupted while waiting |
tryLock() | Acquire only if free right now; returns true/false immediately |
tryLock(time, unit) | Wait up to a timeout; interruptible |
unlock() | Release |
newCondition() | Create a Condition (a separate wait set) bound to this lock |
#ReentrantLock
class Counter {
private final ReentrantLock lock = new ReentrantLock();
private int count;
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock(); // ALWAYS in finally
}
}
}Like intrinsic locks, ReentrantLock is reentrant — the owning thread can lock it again, and must unlock it the same number of times. getHoldCount(), isLocked() and isHeldByCurrentThread() help with debugging and assertions.
tryLock — Don't Wait Forever
if (lock.tryLock(500, TimeUnit.MILLISECONDS)) {
try {
updateInventory();
} finally {
lock.unlock();
}
} else {
throw new ServiceBusyException("Inventory is busy, try again");
}Useful for deadlock avoidance (see the previous chapter) and for failing fast under contention instead of piling up blocked request threads.
lockInterruptibly — Cancellable Waiting
lock.lockInterruptibly(); // a thread blocked here can be cancelled with interrupt()
try {
process();
} finally {
lock.unlock();
}A thread blocked on synchronized or lock() ignores interrupts until it gets the lock; lockInterruptibly() lets executors cancel it (shutdownNow(), future.cancel(true)).
Fairness
ReentrantLock fairLock = new ReentrantLock(true);| Unfair (default) | Fair |
|---|---|
| A newly arriving thread may "barge" ahead of waiting threads | Longest-waiting thread gets the lock next |
| Much higher throughput | Lower throughput |
| Possible starvation | Prevents starvation |
Use fairness only when starvation is a real problem — the throughput cost is significant. (Note: tryLock() without a timeout ignores fairness.)
#Condition Variables
A Condition is like wait/notify, but a lock can have several of them — so producers and consumers can wait in separate queues:
class BoundedBuffer<T> {
private final Queue<T> items = new ArrayDeque<>();
private final int capacity;
private final ReentrantLock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition();
private final Condition notEmpty = lock.newCondition();
BoundedBuffer(int capacity) { this.capacity = capacity; }
public void put(T item) throws InterruptedException {
lock.lock();
try {
while (items.size() == capacity) {
notFull.await(); // producers wait here
}
items.add(item);
notEmpty.signal(); // wake exactly one consumer
} finally {
lock.unlock();
}
}
public T take() throws InterruptedException {
lock.lock();
try {
while (items.isEmpty()) {
notEmpty.await(); // consumers wait here
}
T item = items.remove();
notFull.signal(); // wake exactly one producer
return item;
} finally {
lock.unlock();
}
}
}Object monitor | Condition |
|---|---|
wait() | await() |
notify() | signal() |
notifyAll() | signalAll() |
| One wait set per object | Many conditions per lock |
The same rules apply: hold the lock, and await() in a while loop. This is essentially how ArrayBlockingQueue is implemented.
#ReentrantReadWriteLock
Many workloads are read-heavy — a cache read thousands of times per second and refreshed occasionally. An exclusive lock makes readers wait for each other needlessly. A read-write lock allows many concurrent readers or one writer:
class ProductCache {
private final Map<Long, Product> cache = new HashMap<>();
private final ReentrantReadWriteLock rw = new ReentrantReadWriteLock();
private final Lock readLock = rw.readLock();
private final Lock writeLock = rw.writeLock();
public Product get(long id) {
readLock.lock(); // many threads can hold this at once
try {
return cache.get(id);
} finally {
readLock.unlock();
}
}
public void put(long id, Product p) {
writeLock.lock(); // exclusive: waits for all readers to leave
try {
cache.put(id, p);
} finally {
writeLock.unlock();
}
}
}| Held | Another read lock? | Another write lock? |
|---|---|---|
| Read lock(s) | ✅ Allowed | ❌ Blocks |
| Write lock | ❌ Blocks | ❌ Blocks |
#StampedLock
StampedLock (Java 8) adds optimistic reads — a read that takes no lock at all, then validates that no write happened:
class Point {
private double x, y;
private final StampedLock sl = new StampedLock();
double distanceFromOrigin() {
long stamp = sl.tryOptimisticRead(); // no locking
double cx = x, cy = y;
if (!sl.validate(stamp)) { // a write happened — fall back
stamp = sl.readLock();
try {
cx = x;
cy = y;
} finally {
sl.unlockRead(stamp);
}
}
return Math.hypot(cx, cy);
}
void move(double dx, double dy) {
long stamp = sl.writeLock();
try {
x += dx;
y += dy;
} finally {
sl.unlockWrite(stamp);
}
}
}Very fast for read-mostly data, but StampedLock is not reentrant, has no Condition support, and is easy to misuse. Use it only for measured hot spots.
#synchronized vs ReentrantLock
| Feature | synchronized | ReentrantLock |
|---|---|---|
| Syntax | Keyword; block structured | API calls; must unlock() in finally |
| Automatic release | ✅ On block exit or exception | ❌ Manual |
| Reentrant | ✅ | ✅ |
| Try without blocking / timeout | ❌ | ✅ tryLock(), tryLock(timeout) |
| Interruptible while waiting | ❌ | ✅ lockInterruptibly() |
| Fairness option | ❌ | ✅ |
| Multiple wait sets | ❌ One per monitor | ✅ Many Conditions |
| Lock across methods | ❌ Must be in one block | ✅ Lock in one method, unlock in another (use carefully) |
| Inspection | Thread.holdsLock() only | isLocked(), getQueueLength(), … |
| Performance | Comparable on modern JVMs | Comparable |
#Interview Questions
Q1What is the difference between synchronized and ReentrantLock?
Both are reentrant mutual-exclusion locks with the same memory semantics. ReentrantLock adds tryLock (with timeout), interruptible locking, optional fairness and multiple Conditions, but must be unlocked manually in finally. synchronized is simpler and releases automatically.
Q2Why must unlock() be in a finally block?
Because an explicit lock isn't released on exceptions. Without finally, an exception leaves the lock held forever and blocks every other thread.
Q3What is a fair lock?
A lock that grants access in roughly first-come-first-served order to the longest-waiting thread, preventing starvation at the cost of throughput.
Q4What is a ReadWriteLock and when do you use it?
A pair of locks allowing many concurrent readers or one exclusive writer. Use it for read-heavy data with infrequent writes and non-trivial read sections.
Q5Can you upgrade a read lock to a write lock in ReentrantReadWriteLock?
No — it deadlocks. Release the read lock first, then acquire the write lock (and re-check state). Downgrading from write to read is supported.
Q6What is a Condition?
A wait set associated with a Lock, providing await(), signal() and signalAll(). A lock can have several conditions, allowing different kinds of waiters to be signalled separately.
Q7What is StampedLock's optimistic read?
A read that takes no lock: it gets a stamp, reads the fields, then calls validate(stamp). If a write occurred, it falls back to a real read lock.
#Common Mistakes
- Forgetting
unlock(), or not putting it infinally. - Calling
lock()inside thetry— iflock()fails,finallycallsunlock()on a lock you don't hold. - Trying to upgrade a read lock to a write lock.
- Using fair locks by default — large throughput loss for no benefit.
- Calling
await()/signal()without holding the lock →IllegalMonitorStateException. - Using
StampedLockreentrantly — it isn't reentrant and will deadlock.
#Best Practices
- Use
synchronizedunless you need a feature onlyLockprovides. - Always follow the
lock(); try { … } finally { unlock(); }idiom. - Use
tryLock(timeout)on request paths where blocking indefinitely is unacceptable. - Use separate
Conditions for different wait reasons. - Measure before introducing read-write or stamped locks; consider concurrent collections first.
#Key Takeaways
ReentrantLockadds timeouts, interruptibility, fairness and multiple conditions to whatsynchronizedoffers.- Explicit locks must be released in
finally. Conditiongives each wait reason its own queue — precise signalling instead ofnotifyAll().ReentrantReadWriteLocklets readers share; writers are exclusive; upgrading deadlocks.StampedLockoffers lock-free optimistic reads for read-mostly hot spots.