Deadlock
A deadlock occurs when two or more threads are each waiting for a lock that another thread already holds — so none of them can ever make progress. It’s one of the trickiest bugs in concurrent programming because the program doesn’t crash; it just silently freezes.
What Causes a Deadlock?
Deadlock requires four conditions to be true at the same time (known as the Coffman conditions):
| Condition | Meaning |
|---|---|
| Mutual exclusion | A resource can be held by only one thread at a time |
| Hold and wait | A thread holding one lock tries to acquire another |
| No preemption | Locks can’t be forcibly taken away from a thread |
| Circular wait | Thread A waits for Thread B’s lock, and Thread B waits for Thread A’s lock |
Break any one of these conditions and deadlock cannot occur.
A Classic Deadlock Example
public class DeadlockDemo {
private static final Object LOCK_A = new Object();
private static final Object LOCK_B = new Object();
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
synchronized (LOCK_A) {
System.out.println("Thread-1: holding Lock A, waiting for Lock B...");
try { Thread.sleep(100); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
synchronized (LOCK_B) {
System.out.println("Thread-1: acquired both locks");
}
}
}, "Thread-1");
Thread thread2 = new Thread(() -> {
synchronized (LOCK_B) {
System.out.println("Thread-2: holding Lock B, waiting for Lock A...");
try { Thread.sleep(100); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
synchronized (LOCK_A) {
System.out.println("Thread-2: acquired both locks");
}
}
}, "Thread-2");
thread1.start();
thread2.start();
}
}
Output:
Thread-1: holding Lock A, waiting for Lock B...
Thread-2: holding Lock B, waiting for Lock A...
(program hangs forever)
Thread-1 grabs LOCK_A and waits for LOCK_B. Thread-2 grabs LOCK_B and waits for LOCK_A. Neither can proceed — classic circular wait.
How to Detect a Deadlock
Using jstack
While your deadlocked program is running, open a terminal and run:
jstack <pid>
Look for "Found one Java-level deadlock" in the output. It will show you exactly which threads are waiting on which monitors — invaluable for diagnosing production issues.
Using ThreadMXBean Programmatically
You can detect deadlocks at runtime using the ThreadMXBean API:
import java.lang.management.ManagementFactory;
import java.lang.management.ThreadMXBean;
public class DeadlockDetector {
public static void detectDeadlocks() {
ThreadMXBean bean = ManagementFactory.getThreadMXBean();
long[] deadlockedIds = bean.findDeadlockedThreads(); // returns null if none
if (deadlockedIds != null) {
System.out.println("Deadlock detected! Thread IDs: ");
for (long id : deadlockedIds) {
System.out.println(" " + bean.getThreadInfo(id).getThreadName());
}
} else {
System.out.println("No deadlock detected.");
}
}
public static void main(String[] args) throws InterruptedException {
// (start deadlocked threads here, then call:)
Thread.sleep(500);
detectDeadlocks();
}
}
Tip:
findDeadlockedThreads()detects threads stuck onsynchronizedblocks ANDjava.util.concurrentlocks. UsefindMonitorDeadlockedThreads()if you only care aboutsynchronizedmonitors.
Preventing Deadlock
1. Consistent Lock Ordering
The simplest fix: always acquire locks in the same order across all threads. This eliminates circular wait.
public class ConsistentLockOrder {
private static final Object LOCK_A = new Object();
private static final Object LOCK_B = new Object();
// Both threads now lock A first, then B — no circular wait possible
static void doWork(String name) {
synchronized (LOCK_A) {
synchronized (LOCK_B) {
System.out.println(name + ": working with both locks");
}
}
}
public static void main(String[] args) {
new Thread(() -> doWork("Thread-1")).start();
new Thread(() -> doWork("Thread-2")).start();
}
}
Output:
Thread-1: working with both locks
Thread-2: working with both locks
2. Using tryLock() with a Timeout
The ReentrantLock class lets you attempt to acquire a lock without blocking forever:
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.TimeUnit;
public class TryLockDemo {
private static final ReentrantLock lockA = new ReentrantLock();
private static final ReentrantLock lockB = new ReentrantLock();
static void transfer(String name) throws InterruptedException {
boolean acquiredA = false;
boolean acquiredB = false;
try {
acquiredA = lockA.tryLock(50, TimeUnit.MILLISECONDS);
acquiredB = lockB.tryLock(50, TimeUnit.MILLISECONDS);
if (acquiredA && acquiredB) {
System.out.println(name + ": both locks acquired, doing work");
} else {
System.out.println(name + ": could not acquire both locks, backing off");
}
} finally {
if (acquiredA) lockA.unlock();
if (acquiredB) lockB.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
transfer("Thread-1");
}
}
If a thread can’t get both locks within the timeout, it releases what it holds and backs off — breaking the “hold and wait” condition.
Warning:
tryLock()with a timeout prevents deadlock but can cause livelock if both threads keep retrying at the same time. Add a random back-off delay to avoid this pattern.
3. Avoid Nested Locking
If you don’t need two locks at the same time, don’t hold them simultaneously. Finish with one lock before acquiring the next.
4. Reduce Lock Scope
Keep synchronized blocks as short as possible. The less time a thread holds a lock, the lower the chance of contention.
// Bad: holding lock while doing slow I/O
synchronized (this) {
loadDataFromDisk(); // slow!
process();
}
// Better: only lock what truly needs protection
loadDataFromDisk(); // outside the lock
synchronized (this) {
process(); // fast, in-memory operation
}
5. Use Higher-Level Concurrency Utilities
The java.util.concurrent package provides thread-safe structures that handle locking internally — reducing the surface area for manual lock mistakes. See the concurrent collections page for examples.
Under the Hood
When a thread calls synchronized (obj), the JVM attempts to acquire the object monitor — a per-object structure inside the object header (in HotSpot, stored in the mark word of the object’s header). If the monitor is already held by another thread, the requesting thread is parked in a contention queue and put to sleep by the OS.
Deadlock is a purely logical condition — the JVM does not automatically break deadlocks. The deadlocked threads remain in the BLOCKED state indefinitely. This is why tools like jstack and ThreadMXBean are so important: they inspect the JVM’s internal wait-for graph to find cycles.
With virtual threads (Project Loom) in Java 21, pinning (a virtual thread blocking a carrier OS thread) can worsen deadlock-like situations if virtual threads call synchronized on the same monitors. The recommendation for virtual-thread code is to use ReentrantLock instead of synchronized wherever possible.
Note: Java does not have a built-in deadlock prevention mechanism at the language level. Preventing deadlock is entirely the programmer’s responsibility.
Quick Reference: Deadlock Prevention Strategies
| Strategy | How it breaks Coffman | Best for |
|---|---|---|
| Consistent lock ordering | Eliminates circular wait | Simple, well-known lock sets |
tryLock() with timeout | Breaks hold-and-wait | Complex or dynamic lock sets |
| Single lock / lock coarsening | Eliminates hold-and-wait | Low-contention code |
| Concurrency utilities | Eliminates manual locking | Collections, queues, pools |
| Reduce lock scope | Reduces contention window | Any synchronized code |
Related Topics
- Synchronization — the foundation of locking in Java that makes deadlocks possible
- Synchronized Block — fine-grained locking to minimize lock scope
- ReentrantLock & Monitors —
tryLock()and other advanced lock features that help prevent deadlock - Inter-Thread Communication —
wait()/notify()patterns that can also deadlock if misused - Java Memory Model — the memory visibility rules that underpin all synchronization
- Concurrent Collections — thread-safe data structures that reduce manual locking