Deadlocks in Java Concurrency
A deadlock occurs when concurrent threads permanently wait for resources held by one another. In Java backend interviews, lock ordering, resource ownership, and practical diagnosis are key parts of explaining and preventing deadlocks.
A deadlock requires a cycle of waiting: each participant holds at least one resource while waiting for another resource that another participant holds. With Java's intrinsic monitors or explicit `Lock` implementations, this can leave threads blocked indefinitely unless some external mechanism breaks the cycle.
A common pattern is inconsistent lock ordering. Suppose thread A acquires `lock1` and then waits for `lock2`, while thread B acquires `lock2` and then waits for `lock1`. Neither thread can proceed, because each is waiting for the other to release a lock.
The most practical prevention technique is to establish a global lock-ordering rule and follow it everywhere. Other approaches include reducing lock scope, avoiding nested locks when possible, using timed acquisition such as `tryLock`, and choosing higher-level concurrency utilities instead of manually coordinating multiple locks.
Diagnosis usually starts with a thread dump. Look for threads in `BLOCKED` or lock-wait states and inspect which monitor or lock each thread owns and which one it is attempting to acquire. Java diagnostic tools and JVM thread dumps can reveal the dependency cycle, while production symptoms often include stalled requests, rising latency, and threads that stop making progress.
Key points
- Deadlock requires cyclic waiting among competing resource holders.
- Inconsistent lock ordering is a common cause.
- Use a consistent global lock order to prevent lock cycles.
- Keep critical sections small and avoid unnecessary nested locking.
- Thread dumps can expose blocked threads and their lock dependencies.
Interview questions
What is a deadlock, and what conditions are required for one to occur?
A deadlock is a state where a group of threads cannot progress because each is waiting for a resource held by another participant. The classic conditions are mutual exclusion, hold-and-wait, no preemption, and circular wait. Breaking any one of these conditions can prevent a deadlock. In Java, nested monitor or lock acquisition can create these conditions.
How would you prevent deadlocks in a Java service?
I would first establish a consistent ordering for acquiring multiple locks and enforce it across the codebase. I would also minimize lock scope and avoid holding locks during slow operations such as network or database calls. Where appropriate, I would use timed `tryLock` operations so a thread can abandon an acquisition attempt rather than waiting indefinitely. Higher-level concurrency abstractions can also reduce manual locking.
How would you diagnose a deadlock in production?
I would capture thread dumps while the system is stalled and inspect threads that are blocked or waiting for locks. The important evidence is which locks each thread owns and which lock it is attempting to acquire. I would reconstruct the dependency graph and look for a cycle. After identifying the code paths responsible, I would reproduce the acquisition pattern under controlled concurrency and then fix the ownership or ordering problem.
What is the difference between `synchronized` and `ReentrantLock` when thinking about deadlocks?
Both can participate in deadlocks when multiple locks are acquired in conflicting orders. `synchronized` uses intrinsic object monitors and releases the monitor automatically when the synchronized block or method exits. `ReentrantLock` provides additional control, including timed or interruptible acquisition through its APIs. Those capabilities can make certain deadlock-handling strategies easier, but they do not automatically prevent deadlocks.
Can a deadlock be fixed simply by increasing thread-pool size?
No. Increasing the pool size does not remove a circular dependency between locks. It may change timing and make the symptom appear less frequently, but the underlying cycle can remain. The correct fix is to remove the dependency cycle or otherwise introduce a bounded acquisition and recovery strategy.