Detailed explanation
A deadlock occurs when threads become permanently blocked because each is waiting for a resource held by another thread in a circular dependency. In Java, this commonly involves intrinsic locks used by synchronized or explicit locks such as ReentrantLock. For example, thread T1 holds lockA and waits for lockB, while thread T2 holds lockB and waits for lockA. Neither can reach the code that releases its current lock. Unlike ordinary contention, where a thread eventually acquires a busy lock, deadlock prevents the affected threads from making progress.
Four conditions enable classical resource deadlock: mutual exclusion, hold-and-wait, no forced resource preemption, and circular wait. Java monitors provide exclusive ownership, and entering a synchronized block does not automatically release locks already held. If two threads acquire nested locks in opposite orders, a particular scheduling interleaving can complete the cycle. This explains why code may pass tests repeatedly and still deadlock in production. Reentrancy only allows a thread to reacquire a lock it already owns; it does not prevent cycles involving different threads.
The strongest general prevention technique is a consistent global lock order. If every code path acquires lockA before lockB, neither thread can hold lockB while waiting to acquire lockA. For dynamically selected resources, such as bank accounts, define an order using stable, unique identifiers. Also reduce nested locking, keep critical sections short, and avoid calling unknown code while holding locks. ReentrantLock.tryLock with a timeout supports a recoverable acquisition strategy: if acquiring the second lock fails, release the first lock and retry later or abandon the operation. Always release acquired explicit locks in finally blocks. A timeout alone is insufficient if the failure path retains other locks.
Interviewers typically expect a concrete two-thread example, the circular-wait explanation, and a prevention strategy with its limitations. For diagnosis, inspect thread dumps or use ThreadMXBean.findDeadlockedThreads() for supported monitor and ownable-synchronizer deadlocks. Java does not automatically resolve deadlocks, and synchronized monitor acquisition cannot be timed out or interrupted. Prevention is therefore more reliable than attempting recovery after a deadlock occurs.
Key points to remember
- Deadlock involves a circular dependency that indefinitely blocks the affected threads.
- The four enabling conditions are mutual exclusion, hold-and-wait, no forced preemption, and circular wait.
- Enforce the same global lock-acquisition order across every relevant code path.
- Use timed tryLock with cleanup and a retry or abort policy; reduce nested locking.
- Diagnose lock cycles using thread dumps or ThreadMXBean.findDeadlockedThreads().
Common mistakes candidates make
- Assuming synchronized or reentrant locks automatically prevent deadlock.
- Adding a tryLock timeout without releasing previously acquired locks when acquisition fails.
- Treating shorter critical sections or successful stress tests as proof that deadlock is impossible.
Likely follow-up questions
How does deadlock differ from livelock and starvation?
In deadlock, affected threads cannot proceed because of a circular dependency; in livelock, they keep reacting or retrying without useful progress. Starvation occurs when a thread is repeatedly denied resources or execution opportunities while other threads progress.
Can interrupting a thread break a deadlock?
A thread waiting to enter a synchronized block does not stop waiting when interrupted. Explicit-lock acquisition through lockInterruptibly or timed tryLock can respond to interruption, but recovery still requires releasing any other locks held.
How would you safely lock two accounts during a transfer?
Acquire their locks in ascending order of stable, unique account IDs, using that same rule for every operation needing both locks. Handle a transfer to the same account separately, and release explicit locks in finally blocks.
Interview tip: Walk through T1 holding lockA and T2 holding lockB, explain the resulting cycle, then show how acquiring lockA before lockB everywhere breaks it.
