When “same time” breaks your design
One variable, CAS
Critical sections
Handoff + wait
N permits
Analogy: one bathroom key
A mutex is the single bathroom key at a gas station: whoever holds it can enter; everyone else waits. Atomics are a tiny lock for one dial. A queue is the take-a-number system so people don’t busy-wait at the counter.
Fundamentals: shared memory and interleaving
The toolbox (quick reference)
- Atomics — thread-safe ops on one variable (CAS). Counters, flags. Fail when two fields must stay consistent. → Correctness.
- Locks (mutexes) — mutual exclusion for a critical section. Default for check-then-act and multi-field updates. Coarse / fine / read-write. → Correctness.
- Semaphores — counting permits. Cap concurrent downloads or in-flight API calls. → Scarcity.
- Condition variables — wait efficiently until a condition is true (release lock + sleep; wake on signal). Foundation for blocking queues; rarely coded raw in interviews. → Coordination.
- Blocking queues — thread-safe put/take; block when full/empty. Default for producer-consumer and for pooling actual resource objects. → Coordination & Scarcity.
from threading import Lock, Semaphore
from queue import Queue
from concurrent.futures import ThreadPoolExecutor # often enough in Python
# Atomics: Python has no AtomicInteger — use a Lock, or multiprocessing.Value
# Lock
lock = Lock()
with lock:
balance += amount
# Semaphore — at most 5 concurrent ops
slots = Semaphore(5)
# Blocking queue — capacity enables backpressure
q: Queue = Queue(maxsize=1000)
q.put(task) # blocks if full
task = q.get() # blocks if empty
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReentrantLock;
// Atomics
AtomicInteger balance = new AtomicInteger(0);
balance.addAndGet(amount);
// Lock
ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
// critical section
} finally {
lock.unlock();
}
// Semaphore — at most 5 concurrent ops
Semaphore slots = new Semaphore(5);
// Blocking queue — capacity enables backpressure
BlockingQueue<Runnable> q = new ArrayBlockingQueue<>(1000);
q.put(task); // blocks if full
Runnable t = q.take(); // blocks if empty
// Thread pool
ExecutorService pool = Executors.newFixedThreadPool(8);
Three problem types
- Correctness — shared state corrupted. Two threads book one seat. Fix: locks, atomics, thread confinement. Patterns: check-then-act, read-modify-write.
- Coordination — threads need ordering or handoff. Producers enqueue; consumers wait without spinning. Fix: blocking queues, actors, event loops.
- Scarcity — resources are limited (10 connections, 100 requests). Fix: semaphores, resource pools.
Worked mini-example: two bookers, one seat
# Broken
if seat.free:
seat.free = False # race: both threads pass the check
# Fixed (coarse)
with show.lock:
if not seat.free:
raise RuntimeError("taken")
seat.free = False
// Broken
if (seat.free) {
seat.free = false; // race: both threads pass the check
}
// Fixed (coarse)
synchronized (show.lock) {
if (!seat.free) {
throw new IllegalStateException("taken");
}
seat.free = false;
}
This is the entire reason concurrency appears in LLD: check-then-act on shared mutable state.
Concurrency basic anti-patterns
- “Java is thread-safe” as a design.
- Synchronizing every getter.
- Starting with lock-free atomics before a mutex version.
- Discussing CPUs/cores instead of the invariant.
Map checklist
- Is the problem correctness, coordination, or scarcity?
- What is the shared mutable state?
- What is the invariant in one sentence?
- What’s the coarsest lock that works for v1?
Common interview pitfalls
These mistakes show up constantly on this prompt. Name the trap, then show the fix in your design — don’t wait for the interviewer to catch you.
- Jumping to scarcity/coordination without shared-state invariant.
- “Immutable everything” as a dodge when mutation is required.
- Confusing parallelism speedup with race freedom.
- No example problem — only definitions.
- Saying threads are bad / async is always better.
Interview script (say this)
Read this once out loud before a mock. It’s the spine of a strong answer — not a script to recite robotically.
- Concurrency means overlapping execution touching shared state.
- Three families: correctness, coordination, scarcity.
- I’ll always state the invariant first.
- Example: two bookers, one seat — check-then-act.
- v1 tool is a mutex around the critical section.
- Then we can discuss queues or pools if the prompt needs them.
- I map the follow-up to the right family before naming primitives.
- Basics page is the map; deep dives are the drills.
Extra verification traces
Walk these three traces on the board. If you can narrate them cleanly, your implementation section usually follows.
Staff-level follow-ups
At staff+, they twist the prompt. Answer in one sentence that names the seam — don’t redesign the whole board.
- Where to go deeper? — Correctness first, then coordination/scarcity hubs.
- Language specifics? — Speak invariants; map to synchronized/Lock/asyncio as needed.
- Async only shop? — Same races exist across tasks — locks or confinement still apply.
Interleaving worksheet (draw this)
# Shared: seat = AVAILABLE
# Alice Bob
# read seat → AVAILABLE
# read seat → AVAILABLE
# write seat = BOOKED(Alice)
# write seat = BOOKED(Bob) # last write wins
# return True return True
# → both think they own the seat; only Bob is in memory
# Fixed: one lock around check+write
# Alice Bob
# acquire lock
# read AVAILABLE → book Alice
# release lock
# acquire lock
# read BOOKED → return False
# release lock
// Shared: seat = AVAILABLE
// Alice Bob
// read seat → AVAILABLE
// read seat → AVAILABLE
// write seat = BOOKED(Alice)
// write seat = BOOKED(Bob) // last write wins
// return true return true
// → both think they own the seat; only Bob is in memory
// Fixed: one lock around check+write
// Alice Bob
// acquire lock
// read AVAILABLE → book Alice
// release lock
// acquire lock
// read BOOKED → return false
// release lock
Complete mini-solution: seat booking
from threading import Lock
from typing import Optional
class SeatBooking:
def __init__(self, seat_ids: list[str]):
self._owners: dict[str, str] = {} # seat → user
self._valid = set(seat_ids)
self._lock = Lock()
def book(self, seat_id: str, user_id: str) -> bool:
if seat_id not in self._valid:
raise ValueError(f"unknown seat {seat_id}")
with self._lock:
if seat_id in self._owners:
return False
self._owners[seat_id] = user_id
return True
def cancel(self, seat_id: str, user_id: str) -> bool:
with self._lock:
if self._owners.get(seat_id) != user_id:
return False
del self._owners[seat_id]
return True
def owner_of(self, seat_id: str) -> Optional[str]:
with self._lock:
return self._owners.get(seat_id)
# Upgrade path (say aloud): dict[seat_id, Lock] + ordered multi-seat
# acquire to avoid deadlock — only if coarse lock becomes a hotspot.
import java.util.*;
import java.util.concurrent.locks.ReentrantLock;
class SeatBooking {
private final Map<String, String> owners = new HashMap<>(); // seat → user
private final Set<String> valid;
private final ReentrantLock lock = new ReentrantLock();
SeatBooking(List<String> seatIds) {
this.valid = new HashSet<>(seatIds);
}
boolean book(String seatId, String userId) {
if (!valid.contains(seatId)) {
throw new IllegalArgumentException("unknown seat " + seatId);
}
lock.lock();
try {
if (owners.containsKey(seatId)) return false;
owners.put(seatId, userId);
return true;
} finally {
lock.unlock();
}
}
boolean cancel(String seatId, String userId) {
lock.lock();
try {
if (!userId.equals(owners.get(seatId))) return false;
owners.remove(seatId);
return true;
} finally {
lock.unlock();
}
}
String ownerOf(String seatId) {
lock.lock();
try {
return owners.get(seatId);
} finally {
lock.unlock();
}
}
}
// Upgrade path (say aloud): ConcurrentHashMap + per-seat ReentrantLock
// with ordered multi-seat acquire to avoid deadlock — only if coarse lock is a hotspot.
Which classic needs which lock?
- Parking / locker / library copy — coarse lock on the bank/lot (find + claim). → Correctness.
- Movie / BookMyShow seats — per-show lock; never across payment. → Correctness + hold TTL.
- Rate limiter — lock per client key (or shard); refill+consume atomic. → Correctness.
- Inventory transfer — ordered multi-lock (warehouse id ascending). → Correctness (deadlock).
- Logger async — bounded queue + worker. → Coordination.
- Connection / thread pool — semaphore or blocking queue of resources. → Scarcity.
- Chess / Tic-Tac-Toe / Connect Four — usually single-threaded game; if HTTP API, one lock per Game id.