Concurrency · LLD
Locks and monitors
A lock (monitor) makes other threads wait while one thread updates shared state. Choose what you lock and how long you hold it. Example: Gate A holds the occupancy lock. Gate B waits. If Gate A locks the entire ParkingLot service while it prints a receipt and sends SMS, Gate B waits for printing and SMS — not for a free spot.
If Gate A locks only the pick-and-occupy decision, Gate B waits briefly, then takes spot 102. This page is and , after the conflict story on optimistic versus pessimistic.
The real question
What is the smallest thing I must lock, and how long do I hold it?
Choose how big a lock you need
Mall, lot, spot
Too big
synchronized (parkingLot) { occupy; print; sms; } — Gate B waits for the network.
Better
Lock lot → pick + occupy → unlock → print. The other gate waits for a blink of list walking.
Smallest
CAS on the spot when pick can retry. No wait. Loser walks to 102. Lock only if several fields must move together.
What a lock is in Java
A lock controls access to shared state. The object you synchronize on is that lock. In Java that object is called a . Two threads locking different objects do not wait for each other. Two threads locking a receipt printer do not protect occupancy. Synchronize on the object that owns the racing state.
CAS came first in this chapter because many LLD races are a single arrow. Use a lock when several fields must change together and a single compareAndSet is not enough — a cache get that moves a node and a put that evicts, a transfer that debits and credits. One lock on that object is an honest interview answer. Finer locking is a follow-up.
Lock the decision, not the printer
Reason first: pick-and-occupy must not interleave, print may. Then the Java is lock, try, finally unlock. Never lock() without finally. InterruptedException is not an excuse to skip unlock.
class ParkingLot {
private final ReentrantLock lock = new ReentrantLock();
private final List<ParkingSpot> spots;
ParkingSpot park(Vehicle v) {
lock.lock();
try {
for (ParkingSpot s : spots) {
if (s.fits(v) && s.isFree()) {
s.occupy();
return s;
}
}
throw new LotFullException();
} finally {
lock.unlock();
}
}
}
// issueTicket and sms live in the caller, after park() returns.How long to hold the lock — and nested locks
Hold the lock only for the decision. Notify, I/O, or another lock inside the critical section invites deadlock and long waits. Nested locks need a global . Prefer not nesting. saves you from self-deadlock on the same monitor; it does not save you from A-then-B versus B-then-A.
Deadlock sketch
Transfer locks account 7 then 3. Reverse transfer locks 3 then 7. Both wait. Sort ids, or lock a transfer object instead of two accounts.
tryLock
is a fail path with a clock. Use it when “busy” is better than a queue. Do not use it to paper over the wrong granularity.
synchronized versus ReentrantLock
In an LLD interview, synchronized on the right object is enough. Reach for ReentrantLock when you need tryLock, a condition variable, or a fair queue you can defend. Fairness is not a default virtue; it can collapse throughput at the gate. Say why you want it.
synchronized (spot) {
if (spot.isFree()) spot.occupy();
}
// Same idea with ReentrantLock when you need tryLock:
if (lock.tryLock(50, TimeUnit.MILLISECONDS)) {
try { pickAndOccupy(v); }
finally { lock.unlock(); }
} else {
throw new LotBusyException();
}wait/notify is not your first lock
Condition variables exist: wait until a spot frees, then occupy. In Java that is wait/notify on a monitor, or ReentrantLock plus Condition. In an LLD interview, a BlockingQueue or “try the next spot / LotFull” is usually enough. Rolling your own wait loop without a while-guard is a classic missed wakeup. Prefer a concurrent collection that already waits, or a lock around pick with no wait at all.
// Missed wakeup risk — do not invent this unless asked:
synchronized (lot) {
while (!lot.hasFree()) lot.wait();
lot.occupy(v);
lot.notifyAll();
}
// Prefer: tryOccupy loop, or BlockingQueue of free spot ids.Fair locks prevent barge-in so Gate B, waiting longest, runs next. Fairness can collapse throughput at a hot lot lock. Unfair is the default and is usually the right LLD default. Mention fairness only if starvation is the story they asked for.
One lock, several fields
Cache get that bumps recency and maybe evicts: those writes are one picture. A single lock on the cache object is honest. Splitting into a lock per node without a protocol for eviction is how you deadlock or double-evict. Start coarse. Finer locking is a follow-up if they ask about Saturday QPS on the cache itself — still in-process, still one JVM.
Common mistakes
Locking the world
synchronized on the use-case controller makes every gate wait for every other action. Lock the occupancy (or the lot’s pick), then print outside the lock.
Calling out while holding the lock
Notify, I/O, or another lock inside the critical section invites deadlock and long waits. Claim the state, release, then talk to the world.
Locking the printer
The shared fact is occupancy. A monitor on the receipt object leaves both gates free to read FREE. Name the racing state, then name the object you synchronize on.
Read locks, write locks — only if they ask
ReentrantReadWriteLock lets many readers or one writer. A cache that is read-heavy and rarely evicts can use it. Occupancy is write-heavy at the gate; RW locks buy little. StampedLock is a follow-up, not an opening. In LLD, a single ReentrantLock or synchronized on the right object is the default. Mention read-write if they stress many get()s and rare put()s on a cache — and still keep eviction in the write side.
Interruptible lock acquisition matters if a gate thread must cancel. tryLock with a timeout is the usual LLD verb. lockInterruptibly is for servers that honor interrupt. Do not sprinkle it to look complete.
Exercises
- Cache get must bump recency and maybe evict. What do you lock, and what happens if you only CAS the node?
- park() locks the lot, then notify() tries park() on the same thread. Why does reentrancy not explode — and why is this still a bad hold?
- Two accounts, two locks, two orders. Write the deadlock interleaving. What total order fixes it?
- You synchronized on this in ParkingService. Name three actions that now wait on each other that are not occupancy.
Decide what to lock: the whole service, the lot, or one spot. Draw hold time: decision versus SMS. If those two sketches are honest, ReentrantLock versus synchronized is a footnote.
Prefer a private final Object lock over locking a public String. Always unlock in finally. Nested locks need an order you can say in one sentence.
Read next
Lock pitfalls that still break designs
Locking String interned literals or Boolean.TRUE is locking a global. Two unrelated lots can deadlock on the string "spot". Prefer a private final Object lock = new Object() if you do not want the lot’s public API as the lock target. Unlocking twice, or unlocking a lock you do not hold, throws. Forgetting finally after a constructor throw inside the try leaves the lock taken until the thread dies.
A fair lock plus a long hold is a Saturday queue that looks polite and moves like glue. An unfair lock plus tryLock(0) is a spin in disguise if you loop. Measure hold time in your head: list walk of 400 spots is short; SMS is not.
Takeaway
A monitor is a lock object. Lock the shared state that races, only for the pick-and-occupy decision — not the whole service, and not while sending SMS. Prefer CAS for a single arrow. Nest only with a global order. Unlock in finally.
Prefer a private final lock object over interned strings. Unfair locks are the usual default. Prefer BlockingQueue or try-next-spot over hand-rolled wait/notify. Reach for read-write locks only when reads dominate. Hold time is the design choice; the lock class name is secondary. Claim the spot, unlock in finally, then print.
One lock for several fields that must move together is honest. Two locks without an order is a deadlock waiting for Saturday.
Choose lock size first, then the Java lock type, then always unlock in finally.