Deadlock
Deadlock is a situation in which two or more threads are permanently blocked, each waiting to acquire a lock held by another thread in the same cycle. No thread in the cycle can ever make progress because each is waiting for a resource that will never be released. Deadlock is silent — no exception is thrown, no timeout occurs, the threads simply stop doing useful work forever while appearing to be alive. Unlike exceptions or incorrect output, deadlock produces programs that hang, making it one of the most serious concurrency defects. Java's intrinsic locks provide no built-in deadlock prevention or detection, so understanding deadlock fully — its four necessary conditions, reliable detection, proven prevention strategies, and avoidance via lock ordering and tryLock — is essential for writing correct concurrent code. This entry covers the four Coffman conditions, the two-thread canonical example and its generalization to N threads, programmatic deadlock detection via ThreadMXBean, lock ordering as the primary prevention strategy, tryLock with timeout as an escape mechanism, open-call discipline, and the differences between deadlock and related liveness failures.
The Four Coffman Conditions and the Canonical Example
// ── Canonical two-thread deadlock ─────────────────────────────────────
public class TwoThreadDeadlock {
private final Object lockA = new Object();
private final Object lockB = new Object();
// Thread 1: acquires lockA, then tries for lockB
public void methodA() throws InterruptedException {
synchronized (lockA) {
System.out.println(Thread.currentThread().getName() + ": holding A, waiting for B");
Thread.sleep(50); // sleep to increase likelihood of interleaving
synchronized (lockB) {
System.out.println(Thread.currentThread().getName() + ": holding A and B");
}
}
}
// Thread 2: acquires lockB, then tries for lockA — opposite order
public void methodB() throws InterruptedException {
synchronized (lockB) {
System.out.println(Thread.currentThread().getName() + ": holding B, waiting for A");
Thread.sleep(50);
synchronized (lockA) {
System.out.println(Thread.currentThread().getName() + ": holding B and A");
}
}
}
}
TwoThreadDeadlock example = new TwoThreadDeadlock();
Thread t1 = new Thread(() -> {
try { example.methodA(); }
catch (InterruptedException e) { Thread.currentThread().interrupt(); }
}, "Thread-1");
Thread t2 = new Thread(() -> {
try { example.methodB(); }
catch (InterruptedException e) { Thread.currentThread().interrupt(); }
}, "Thread-2");
t1.start(); t2.start();
// Output (then hangs forever):
// Thread-1: holding A, waiting for B
// Thread-2: holding B, waiting for A
// [both threads blocked permanently — program never terminates]
// ── N-thread ring deadlock ─────────────────────────────────────────────
public static void ringDeadlock(int n) {
Object[] locks = new Object[n];
for (int i = 0; i < n; i++) locks[i] = new Object();
for (int i = 0; i < n; i++) {
final int index = i;
new Thread(() -> {
Object first = locks[index];
Object second = locks[(index + 1) % n];
synchronized (first) {
System.out.println(Thread.currentThread().getName()
+ " holds lock " + index + ", wants lock " + (index + 1) % n);
try { Thread.sleep(50); } catch (InterruptedException e) {}
synchronized (second) { // waits for next thread's lock — ring closes
System.out.println(Thread.currentThread().getName() + " got both");
}
}
}, "Thread-" + i).start();
}
}
ringDeadlock(5); // 5-thread ring: T0→L0→L1, T1→L1→L2, ..., T4→L4→L0
// ── Deadlock is silent — the JVM does not throw or terminate ──────────
// A deadlocked thread's state is BLOCKED (not TERMINATED or ERROR)
// Thread.getState() == Thread.State.BLOCKED for deadlocked threads
// ThreadMXBean can detect the cycle — see detection section belowDetection — ThreadMXBean and Thread Dumps
// ── Programmatic detection via ThreadMXBean ───────────────────────────
import java.lang.management.*;
public class DeadlockDetector {
public static void detectAndReport() {
ThreadMXBean threadBean = ManagementFactory.getThreadMXBean();
// findDeadlockedThreads: intrinsic AND j.u.c.locks deadlocks (Java 6+)
long[] deadlockedIds = threadBean.findDeadlockedThreads();
if (deadlockedIds == null) {
System.out.println("No deadlock detected");
return;
}
System.out.println("DEADLOCK DETECTED — " + deadlockedIds.length + " threads involved:");
ThreadInfo[] infos = threadBean.getThreadInfo(deadlockedIds, true, true);
for (ThreadInfo info : infos) {
System.out.printf("%n=== Thread: %s (id=%d) ===%n",
info.getThreadName(), info.getThreadId());
System.out.printf("State: %s%n", info.getThreadState());
System.out.printf("Waiting for lock: %s%n", info.getLockName());
System.out.printf("Lock owned by: %s%n", info.getLockOwnerName());
System.out.println("Stack trace:");
for (StackTraceElement frame : info.getStackTrace()) {
System.out.println(" at " + frame);
}
}
}
// Watchdog thread: polls for deadlock every 5 seconds
public static Thread startWatchdog() {
Thread watchdog = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
detectAndReport();
try { Thread.sleep(5000); }
catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}, "DeadlockWatchdog");
watchdog.setDaemon(true); // don't prevent JVM shutdown
watchdog.start();
return watchdog;
}
}
// ── Usage: inject watchdog into application startup ───────────────────
DeadlockDetector.startWatchdog();
// Now create the deadlock:
TwoThreadDeadlock d = new TwoThreadDeadlock();
new Thread(() -> { try { d.methodA(); } catch (InterruptedException e) {} }).start();
new Thread(() -> { try { d.methodB(); } catch (InterruptedException e) {} }).start();
// After ~5 seconds, watchdog prints:
// DEADLOCK DETECTED — 2 threads involved:
// === Thread: Thread-0 (id=12) ===
// State: BLOCKED
// Waiting for lock: java.lang.Object@1234 (lockB)
// Lock owned by: Thread-1
// Stack trace:
// at TwoThreadDeadlock.methodA(TwoThreadDeadlock.java:14)
// ...
// === Thread: Thread-1 (id=13) ===
// State: BLOCKED
// Waiting for lock: java.lang.Object@5678 (lockA)
// Lock owned by: Thread-0
// ── Thread dump via jstack (command line) ────────────────────────────
// jstack <pid> produces:
//
// Found one Java-level deadlock:
// =============================
// "Thread-1":
// waiting to lock monitor 0x00007f... (object 0x..., a java.lang.Object [lockA]),
// which is held by "Thread-0"
// "Thread-0":
// waiting to lock monitor 0x00007f... (object 0x..., a java.lang.Object [lockB]),
// which is held by "Thread-1"
//
// Java stack information for the threads listed above:
// ===================================================
// "Thread-1":
// at TwoThreadDeadlock.methodB(TwoThreadDeadlock.java:24)
// - waiting to lock <0x...> (lockA)
// - locked <0x...> (lockB)
// "Thread-0":
// at TwoThreadDeadlock.methodA(TwoThreadDeadlock.java:14)
// - waiting to lock <0x...> (lockB)
// - locked <0x...> (lockA)Prevention — Lock Ordering, tryLock, and Open Calls
// ── Lock ordering by System.identityHashCode() ────────────────────────
public class SafeTransfer {
public static void transfer(Account from, Account to, double amount) {
// Derive acquisition order from identity hash code:
int fromHash = System.identityHashCode(from);
int toHash = System.identityHashCode(to);
if (fromHash < toHash) {
synchronized (from) { synchronized (to) { doTransfer(from, to, amount); } }
} else if (fromHash > toHash) {
synchronized (to) { synchronized (from) { doTransfer(from, to, amount); } }
} else {
// Hash collision: use a tie-breaking lock to ensure ordering
synchronized (Account.TIE_LOCK) { // static final Object TIE_LOCK = new Object()
synchronized (from) { synchronized (to) { doTransfer(from, to, amount); } }
}
}
}
private static void doTransfer(Account from, Account to, double amount) {
if (from.getBalance() < amount) throw new IllegalStateException("Insufficient funds");
from.debit(amount);
to.credit(amount);
}
}
// Any two threads calling transfer(A, B) and transfer(B, A) will both
// acquire locks in the same hash-code-determined order — no cycle possible.
// ── tryLock() with timeout and backoff ────────────────────────────────
import java.util.concurrent.locks.*;
import java.util.concurrent.ThreadLocalRandom;
public class TryLockTransfer {
private final ReentrantLock lock;
private double balance;
public TryLockTransfer(double balance) {
this.lock = new ReentrantLock();
this.balance = balance;
}
public static boolean transfer(TryLockTransfer from, TryLockTransfer to, double amount)
throws InterruptedException {
long deadline = System.nanoTime() + 1_000_000_000L; // 1 second total budget
while (true) {
// Try to acquire 'from' lock:
if (from.lock.tryLock(50, TimeUnit.MILLISECONDS)) {
try {
// Try to acquire 'to' lock without blocking forever:
if (to.lock.tryLock(50, TimeUnit.MILLISECONDS)) {
try {
// Both locks acquired — safe to transfer:
if (from.balance < amount) return false;
from.balance -= amount;
to.balance += amount;
return true;
} finally { to.lock.unlock(); }
}
// Could not get 'to' — release 'from' and retry
} finally { from.lock.unlock(); }
}
// Neither held — check timeout and backoff before retry:
if (System.nanoTime() > deadline) return false; // give up
Thread.sleep(ThreadLocalRandom.current().nextLong(1, 10)); // random backoff
}
}
}
// ── Open calls — don't hold locks when calling external code ──────────
public class EventDispatcher {
private final List<EventListener> listeners = new ArrayList<>();
private final Object listenerLock = new Object();
// UNSAFE: holds listenerLock while calling listener — listener may acquire
// another lock in an order that conflicts with some other lock we hold elsewhere
public void fireEventUnsafe(Event event) {
synchronized (listenerLock) {
for (EventListener listener : listeners) {
listener.onEvent(event); // external code called under lock — dangerous
}
}
}
// SAFE: open call — snapshot listeners first, release lock, then call
public void fireEventSafe(Event event) {
List<EventListener> snapshot;
synchronized (listenerLock) {
snapshot = new ArrayList<>(listeners); // copy while locked
}
// Lock released before calling external code:
for (EventListener listener : snapshot) {
listener.onEvent(event); // listener acquires its own locks without interference
}
}
public void addListener(EventListener l) {
synchronized (listenerLock) { listeners.add(l); }
}
}
// ── Lock ordering with an explicit ordering field ─────────────────────
// When objects have natural IDs, use ID order instead of identity hash:
public class OrderedAccount {
private static final AtomicLong ID_COUNTER = new AtomicLong(0);
private final long id = ID_COUNTER.incrementAndGet(); // unique, stable, ordered
private double balance;
public static void transfer(OrderedAccount from, OrderedAccount to, double amount) {
// Always lock the lower-ID account first — guaranteed consistent order:
OrderedAccount first = from.id < to.id ? from : to;
OrderedAccount second = from.id < to.id ? to : from;
synchronized (first) {
synchronized (second) {
from.balance -= amount;
to.balance += amount;
}
}
}
}