Race conditions, deadlock vs livelock with real examples, detecting deadlocks (thread dumps, ThreadMXBean, JFR) and debugging them in production, starvation and why thread priorities are a poor fix, preventing livelock, refactoring ConcurrentModificationException in multithreaded code, designing concurrent access to a shared resource, a deadlock-critical banking transfer design, high-concurrency design principles, and handling 10K concurrent booking requests without overselling.
Published September 25, 2026
These are design and incident questions. Show:
Short answer: A bug where the result depends on the timing and interleaving of threads that access shared state without proper synchronisation. The two classic forms:
count++, where two threads read 5, and both write 6.if (!map.containsKey(k)) map.put(k, v), or "if seats are available, book one", where both threads pass the check.It's non-deterministic: it passes tests, and fails under load. The fixes: make the operation atomic (locks, atomics, computeIfAbsent, database constraints or conditional updates), or remove the sharing (immutability, confinement).
Learn it in depth → Deadlock, Starvation, Livelock
Short answer:
Short answer:
tryLock on resources A and B, in opposite orders. Each fails the second tryLock, releases, and retries immediately. With identical timing, they collide forever.Short answer:
jcmd <pid> Thread.print (or jstack). The JVM reports Java-level deadlocks automatically at the end ("Found one Java-level deadlock"), with the cycle of threads, the locks they hold, and the locks they want. That covers monitors and java.util.concurrent ownable locks.ThreadMXBean.findDeadlockedThreads(). Run it periodically, expose it as a health indicator or metric, and alert on it.tryLock with a timeout, or redesign to avoid nested locks. Restart the affected instance to recover in the meantime.Key points to cover:
ThreadMXBean mx = ManagementFactory.getThreadMXBean();
long[] ids = mx.findDeadlockedThreads();
if (ids != null) for (ThreadInfo info : mx.getThreadInfo(ids, true, true)) log.error("DEADLOCK: {}", info);
Short answer: Starvation is a thread that's perpetually denied the resources it needs (the CPU, a lock, a pool slot), so it makes no progress, even though the system as a whole does. The causes:
Thread priorities (setPriority 1–10) are hints, mapped to OS priorities inconsistently, and often ignored (Linux, by default, ignores them for normal threads). Relying on them is non-portable. Lowering or raising priorities can cause starvation, not reliably prevent it.
Prevention:
ConcurrentModificationException caused by modifying a list while iterating over it, in multithreaded code. How would you refactor it?Short answer: First, understand who owns the list, and the access pattern. Then:
List.copyOf) to the other threads.CopyOnWriteArrayList. Iterators work on snapshots, and are never CME.ConcurrentLinkedQueue, ConcurrentLinkedDeque), or ConcurrentHashMap.newKeySet() for sets. They have weakly consistent iteration.removeIf or Iterator.remove.Common trap: Collections.synchronizedList alone doesn't fix it. You must still synchronized (list) { for (...) } around the iteration.
Short answer: Go through the options, from least to most coordination:
ConcurrentHashMap compound operations, atomics, BlockingQueue hand-offs, and executors.tryLock with timeouts, and read/write or stamped locks for read-heavy data.@Version), conditional updates, unique constraints, or distributed locks (with fencing tokens) sparingly.Short answer: A banking funds-transfer engine, with many threads transferring between accounts.
tryLock(timeout), with a clean failure or retry and jitter, as a safety net.SELECT … FOR UPDATE in ID order (or optimistic versions), and let the database's deadlock detection plus a retry on deadlock errors handle the rest.void transfer(Account a, Account b, BigDecimal amt) {
Account first = a.id() < b.id() ? a : b, second = first == a ? b : a;
synchronized (first) {
synchronized (second) { // same order for every thread, so no cycle
a.debit(amt);
b.credit(amt);
}
}
}
Short answer:
tryLock with timeouts, and immutable data.CompletableFuture composition, or ForkJoin's helping joins);Short answer: Designing systems that stay correct and fast under many simultaneous requests. The principles:
Short answer: Correctness comes from the data layer, and scalability from how you funnel requests.
UPDATE inventory
SET available = available - :qty, version = version + 1
WHERE room_type_id = :id AND stay_date = :date AND available >= :qty;
-- rows updated = 1 → reserved; 0 → sold out (no oversell, no explicit lock)
Or use optimistic locking (@Version) with retries, or SELECT … FOR UPDATE for short transactions. For multi-night stays, update all the date rows in one transaction, in a consistent order.
2. Hot-inventory shortcut: keep counters in Redis (DECRBY, or a Lua script that checks and decrements atomically). Accept the reservation fast, then persist asynchronously through a queue, with reconciliation.
3. Two-phase booking: create a HOLD with a TTL (for example 10 minutes) → payment → CONFIRM. Expired holds release the inventory, through a scheduler or delayed queue.
4. Throttle and queue at the edge: rate limits, a virtual waiting room for flash sales, and Kafka partitioned by inventory key, so each key is processed serially, without contention.
5. Idempotency keys on booking requests, so client retries don't create duplicate bookings.
6. Scale the stateless application tier. Use virtual threads or reactive I/O for the waiting. Size the connection pools to the database's capacity, not to the request count.
7. Observe: conflict rate, hold expiries, oversell alarms (which should always be zero), and reconciliation jobs.
Learn it in depth → Design a Meeting Room / Calendar Booking System
Short answer: Some thread or task never gets the resource it needs (CPU time, a lock, a worker slot), because others keep taking it. It's caused by unfair locks, long-held locks, greedy threads, shared pools saturated by slow tasks, or priority schemes. The cures are fairness, isolation (bulkheads), short critical sections, and bounded waits.
Q: What is a thread-pool-induced deadlock? A: Tasks in a bounded pool submit subtasks to the same pool, and block waiting for their results. Once every worker is waiting, the subtasks sit in the queue forever. Avoid blocking waits inside the pool (compose asynchronously), use ForkJoin (helping joins), or use separate pools.
Q: Does the database protect you from deadlocks?
A: It detects database lock deadlocks, and kills one transaction (for example PostgreSQL's deadlock detected, or MySQL error 1213). Your application must catch it and retry the whole transaction. It can't help with deadlocks between an application lock and a database lock (a thread holding a Java lock while waiting on a row lock held by a thread that wants the Java lock).
Q: Why is holding a lock while calling external code dangerous? A: The callback (a listener, a logging appender, a remote call) may block for a long time (starving others), or try to acquire other locks (deadlock risk). Copy the data under the lock, release it, then call out ("open calls").
Q: How do distributed locks fail, and what's a fencing token? A: Lock leases can expire while the holder pauses (a GC pause, a network delay), so two processes believe they hold the lock. A fencing token (a monotonically increasing number issued with each lock grant) lets the protected resource reject writes carrying an older token.