Deadlocks & Liveness
Deadlocks & Liveness
Getting the synchronized keyword right stops race conditions, but it opens a new category of hazard: liveness failures. A liveness failure means threads stop making progress — not because of a bug in your data, but because the threads themselves are stuck waiting on each other. The three classic forms are deadlock, livelock, and starvation.
Deadlock
A deadlock occurs when two or more threads each hold a lock the other needs, so every thread waits forever and none can proceed. The classic pattern is a lock-ordering problem:
T1 grabs lockA and waits for lockB. T2 grabs lockB and waits for lockA. Neither can continue.
How to Avoid Deadlock
1. Consistent lock ordering. The most reliable fix: every thread always acquires locks in the same global order. If all code acquires A before B, no circular wait can form.
2. Use timed lock attempts. ReentrantLock.tryLock(timeout, unit) lets you back off and retry instead of blocking forever. This turns a deadlock into a recoverable situation:
3. Avoid holding locks while calling unknown code. A common mistake is calling a user-supplied callback or an external method while holding a lock. If that external code acquires another lock, you have an ordering you did not control.
jstack <pid> or ThreadMXBean.findDeadlockedThreads()) reports "Found one Java-level deadlock" with the full stack of each stuck thread. In production, capturing a thread dump is your first diagnostic step when a service stops responding.
Livelock
In a livelock, threads are not blocked — they are actively running — but each one keeps reacting to the other's state change, so no progress is made. Think of two people in a corridor who both step the same way to let the other pass, again and again.
Fix: introduce asymmetry or randomness. Give one thread higher priority, or back off for a random duration (exponential back-off), so both threads do not react identically at the same moment.
Starvation
Starvation means one or more threads are perpetually denied access to a resource because other threads monopolise it. Common causes:
- A high-priority thread keeps running so a low-priority thread never gets CPU time.
- A lock is released and immediately re-acquired by the same thread before a waiting thread can grab it (barging).
- One writer thread continuously holds a read-write lock, starving readers (or vice versa).
A fair ReentrantLock uses a FIFO queue: the thread that has waited the longest gets the lock next. This eliminates starvation but reduces throughput slightly because the JVM can no longer let the current thread (which is already scheduled on a CPU core) re-acquire without a context switch.
java.util.concurrent.
Diagnosing Liveness Problems in Production
- Thread dump:
kill -3 <pid>on Linux orjstack <pid>prints all thread states. Blocked threads show the lock they are waiting on and the thread that owns it. - JConsole / VisualVM / JMC: GUI tools that highlight deadlocked threads graphically.
ThreadMXBean: CallManagementFactory.getThreadMXBean().findDeadlockedThreads()programmatically to detect deadlocks from a monitoring thread or health-check endpoint.
ConcurrentHashMap, BlockingQueue, or a lock-free algorithm) can eliminate the nesting entirely.
Summary
Deadlock is a circular wait between threads holding each other's required locks — fix it with consistent lock ordering or timed tryLock. Livelock is active thrashing with no progress — fix it with randomness or asymmetry. Starvation is a thread perpetually losing the lock race — fix it with a fair lock or redesigned access patterns. All three are liveness failures: the code may be logically correct yet the program stops making useful progress. Recognising which failure you have is the first step to solving it.