Definition
- Deadlock – two or more threads are permanently blocked. Thread-1 holds Lock-A and waits for Lock-B. Thread-2 holds Lock-B and waits for Lock-A. Neither can proceed.
- Coffman conditions – four conditions must all hold for deadlock: mutual exclusion (locks are exclusive), hold-and-wait (hold one lock while waiting for another), no preemption (locks cannot be forcibly taken), circular wait (a cycle of threads waiting for each other).
- Prevention – break any one of the four conditions. The most practical approach is breaking circular wait through consistent lock ordering.
- Detection –
jstackautomatically identifies deadlocks. Programmatically,ThreadMXBean.findDeadlockedThreads()returns the IDs of deadlocked threads.
Teller-1 is inside Rajesh's vault and needs to enter Priya's vault to complete a transfer. Teller-2 is inside Priya's vault and needs to enter Rajesh's vault. Both wait for the other to leave. Neither will leave because they need the other vault first. Both stand frozen forever. The bank halts. The fix is a rule: always enter the vault with the lower account number first.
Understanding the Concepts
What is a deadlock?
A deadlock is a situation where two or more threads are permanently blocked, each waiting for a resource held by the other. No thread can make progress. The system is frozen.
package com.bank.core;
public class DeadlockDemo {
private static final Object lockA = new Object();
private static final Object lockB = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
synchronized (lockA) {
System.out.println("T1: holds lockA, waiting for lockB");
try { Thread.sleep(50); } catch (InterruptedException e) { }
synchronized (lockB) {
System.out.println("T1: holds both locks");
}
}
}, "TransferThread-1");
Thread t2 = new Thread(() -> {
synchronized (lockB) {
System.out.println("T2: holds lockB, waiting for lockA");
try { Thread.sleep(50); } catch (InterruptedException e) { }
synchronized (lockA) {
System.out.println("T2: holds both locks");
}
}
}, "TransferThread-2");
t1.start();
t2.start();
// DEADLOCK: T1 holds A, waits B. T2 holds B, waits A. Forever.
}
}
T1: holds lockA, waiting for lockB
T2: holds lockB, waiting for lockA
(program hangs forever)
How do you fix a deadlock with lock ordering?
Always acquire locks in the same deterministic order. If both threads lock A before B, circular wait is impossible.
package com.bank.core;
public class DeadlockFixDemo {
private static final Object lockA = new Object();
private static final Object lockB = new Object();
public static void main(String[] args) throws InterruptedException {
// Both threads acquire lockA first, then lockB
Thread t1 = new Thread(() -> {
synchronized (lockA) {
synchronized (lockB) {
System.out.println("T1: completed");
}
}
});
Thread t2 = new Thread(() -> {
synchronized (lockA) { // same order as T1!
synchronized (lockB) {
System.out.println("T2: completed");
}
}
});
t1.start(); t2.start();
t1.join(); t2.join();
System.out.println("No deadlock!");
}
}
T1: completed
T2: completed
No deadlock!
How do you detect a deadlock?
The JVM detects deadlocks automatically in thread dumps.
# Take a thread dump
jstack <PID>
Found one Java-level deadlock:
=============================
"TransferThread-1":
waiting to lock monitor 0x00007f... (object 0x000000076...)
which is held by "TransferThread-2"
"TransferThread-2":
waiting to lock monitor 0x00007f... (object 0x000000076...)
which is held by "TransferThread-1"
Programmatically, you can detect deadlocks with ThreadMXBean.
package com.bank.monitoring;
import java.lang.management.ManagementFactory;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;
public class DeadlockDetector {
public static void checkForDeadlocks() {
ThreadMXBean tmx = ManagementFactory.getThreadMXBean();
long[] deadlocked = tmx.findDeadlockedThreads();
if (deadlocked != null) {
ThreadInfo[] infos = tmx.getThreadInfo(deadlocked, true, true);
for (ThreadInfo info : infos) {
System.out.println("DEADLOCK: " + info.getThreadName()
+ " waiting for " + info.getLockName()
+ " held by " + info.getLockOwnerName());
}
}
}
}
What prevention strategies exist?
| Strategy | How it works | Breaks which condition |
|---|---|---|
| Lock ordering | Acquire locks in deterministic order | Circular wait |
| tryLock(timeout) | Give up if lock not available in time | Hold-and-wait |
| Single lock | Use one lock instead of multiple | Hold-and-wait |
| Lock-free | Use AtomicInteger, ConcurrentHashMap | Mutual exclusion |
What about tryLock as a fallback?
ReentrantLock.tryLock(timeout) gives up after a deadline instead of waiting forever.
package com.bank.service;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
public class TryLockTransfer {
public boolean transfer(ReentrantLock fromLock, ReentrantLock toLock) {
try {
boolean gotFrom = fromLock.tryLock(5, TimeUnit.SECONDS);
if (!gotFrom) return false;
try {
boolean gotTo = toLock.tryLock(5, TimeUnit.SECONDS);
if (!gotTo) return false;
try {
// perform transfer
return true;
} finally { toLock.unlock(); }
} finally { fromLock.unlock(); }
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
}
}
What should a senior engineer know?
- Deadlock is permanent: unlike a race condition (which may produce wrong results intermittently), a deadlock is a permanent freeze. The only recovery is restarting the JVM.
- Database deadlocks: databases detect deadlocks and kill one transaction (the victim). Java-level deadlocks have no such automatic resolution.
- Monitoring: schedule
ThreadMXBean.findDeadlockedThreads()every 30 seconds in a health-check thread. Alert via PagerDuty if non-null.
Visual Overview
Deadlock Cycle:
Thread-1 ───holds───▶ Lock-A
│ ▲
│ waits for │ waits for
▼ │
Lock-B ◀───holds─── Thread-2
Both wait forever. Neither can proceed.
Fix: Consistent Lock Ordering
Thread-1: lock(A) ──▶ lock(B) ──▶ done ──▶ unlock(B) ──▶ unlock(A)
Thread-2: lock(A) ──▶ (waits) ──▶ lock(B) ──▶ done ──▶ unlock both
No cycle. Thread-2 waits for A, gets it after Thread-1 finishes.
Key Takeaway
A deadlock occurs when two or more threads permanently block each other, each holding a lock the other needs. The four Coffman conditions must all hold simultaneously: mutual exclusion, hold-and-wait, no preemption, and circular wait. Breaking any single condition prevents deadlock.
The most practical prevention is consistent lock ordering: always acquire locks in the same deterministic order (by account ID, object hash, or any consistent comparator). For two-account transfers, both threads lock the lower account ID first. Detection uses jstack (prints the deadlock chain) or ThreadMXBean.findDeadlockedThreads() (programmatic health check).
Remember one sentence: deadlock is a permanent freeze caused by circular lock dependencies, and the fix is to always acquire locks in a consistent order.
---