A deadlock is the worst kind of concurrency bug: your application does not crash, does not log an error, and does not throw an exception. It just stops. Two threads sit in the BLOCKED state forever, each holding a lock the other one needs, and every request that touches those locks piles up behind them.
If you already understand synchronization in Java, this page takes you one level deeper: why deadlocks happen, how to reproduce one in 30 lines of code, how to read the evidence in a thread dump, and the specific coding rules that make deadlock impossible.
What exactly is a deadlock?
A deadlock occurs when two or more threads are blocked forever, each waiting for a lock held by another thread in the group. The simplest version involves two threads and two locks:
- Thread A acquires lock 1, then tries to acquire lock 2.
- Thread B acquires lock 2, then tries to acquire lock 1.
If both threads complete their first acquisition before either attempts the second, neither can ever proceed. Thread A waits on lock 2 (held by B), and B waits on lock 1 (held by A). The JVM will happily let them wait until the process is killed.
Note the timing dependency: the same code can run cleanly a thousand times and deadlock on run 1,001. That is what makes deadlocks so dangerous in production — they hide during testing because the unlucky interleaving is rare.
The four Coffman conditions
A deadlock can only happen when all four of these conditions hold at the same time. Interviewers love this list because it maps directly to prevention strategies.
| Condition | Meaning | How to break it |
|---|---|---|
| Mutual exclusion | Only one thread can hold the lock at a time | Use lock-free structures or immutable data |
| Hold and wait | A thread holds one lock while waiting for another | Acquire all locks at once, or none |
| No preemption | A lock cannot be forcibly taken from a thread | Use tryLock with a timeout and back off |
| Circular wait | A cycle exists: A waits for B, B waits for A | Impose a global lock-acquisition order |
Breaking any one condition prevents deadlock. In practice, the two techniques you will actually use are breaking circular wait (lock ordering) and breaking no-preemption (tryLock with timeout). The other two are usually impractical for shared mutable state.
A runnable deadlock example
Run this class and it will hang almost every time. Notice the opposite lock order in the two threads — that is the entire bug.
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 t1 = new Thread(() -> {
synchronized (LOCK_A) {
System.out.println("T1 holds A, wants B");
sleep(100); // widen the deadlock window
synchronized (LOCK_B) {
System.out.println("T1 got both");
}
}
}, "worker-1");
Thread t2 = new Thread(() -> {
synchronized (LOCK_B) {
System.out.println("T2 holds B, wants A");
sleep(100);
synchronized (LOCK_A) {
System.out.println("T2 got both");
}
}
}, "worker-2");
t1.start();
t2.start();
}
private static void sleep(long ms) {
try { Thread.sleep(ms); } catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
You will see both "holds" messages print, and then nothing. Neither "got both" line ever appears. Check the thread lifecycle page and you will recognise the state: both threads are BLOCKED, not WAITING — they are stuck on monitor entry.
Common mistake: A common mistake beginners make is thinking
Thread.sleep()causes the deadlock here. Sleep only widens the timing window so the demo fails reliably. The real bug is the inconsistent lock order, and it can deadlock even without any sleep call.
How to detect a deadlock
Thread dumps with jstack
The fastest diagnostic is a thread dump. Find the process ID with jps, then run jstack <pid>. When a monitor deadlock exists, the JVM prints an explicit analysis at the bottom of the dump:
Found one Java-level deadlock:
=============================
"worker-2":
waiting to lock monitor 0x... (object 0x..., a java.lang.Object),
which is held by "worker-1"
"worker-1":
waiting to lock monitor 0x... (object 0x..., a java.lang.Object),
which is held by "worker-2"
The dump names each thread, the lock it wants, and the thread that owns it. Reading this section is a standard task in senior Java interview rounds, so practise it on the demo above.
Programmatic detection with ThreadMXBean
You can also detect deadlocks from inside the application — useful for a health-check endpoint that flags a wedged service:
import java.lang.management.ManagementFactory;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;
public class DeadlockWatchdog {
public static void main(String[] args) throws InterruptedException {
// start DeadlockDemo threads here, then:
Thread.sleep(500);
ThreadMXBean bean = ManagementFactory.getThreadMXBean();
long[] ids = bean.findDeadlockedThreads(); // null when none
if (ids != null) {
for (ThreadInfo info : bean.getThreadInfo(ids)) {
System.out.printf("%s is blocked on %s, held by %s%n",
info.getThreadName(),
info.getLockName(),
info.getLockOwnerName());
}
}
}
}
findDeadlockedThreads() covers both synchronized monitors and java.util.concurrent locks such as ReentrantLock. The older findMonitorDeadlockedThreads() only sees monitors.
Pro tip: Detection tells you a deadlock exists — it cannot fix it. The JVM never preempts a lock, so the only remediation for a live deadlock is restarting the process. Treat detection as an alerting tool and put your real effort into prevention.
How to prevent deadlocks
Strategy 1: consistent lock ordering
The single most effective rule: every thread acquires locks in the same global order. If all code takes LOCK_A before LOCK_B, a circular wait is impossible.
When locks belong to dynamic objects (say, transferring money between two accounts), derive the order from a stable property such as an ID:
public class SafeTransfer {
public void transfer(Account from, Account to, long amount) {
Account first = from.getId() < to.getId() ? from : to;
Account second = from.getId() < to.getId() ? to : from;
synchronized (first) {
synchronized (second) {
from.debit(amount);
to.credit(amount);
}
}
}
}
Whichever direction the transfer runs, both threads lock the lower-ID account first, so the circular wait condition is broken.
Strategy 2: tryLock with timeout
ReentrantLock.tryLock(timeout, unit) breaks the no-preemption condition: if the second lock is not available in time, the thread releases what it holds and retries or fails cleanly instead of blocking forever.
if (lock1.tryLock(50, TimeUnit.MILLISECONDS)) {
try {
if (lock2.tryLock(50, TimeUnit.MILLISECONDS)) {
try {
doWork();
} finally { lock2.unlock(); }
}
} finally { lock1.unlock(); }
}
Strategy 3: reduce locking altogether
- Keep synchronized blocks small and never call unknown (alien) code while holding a lock.
- Prefer concurrent collections — see HashMap vs ConcurrentHashMap — over manually locked maps.
- Use immutable objects and thread confinement so many code paths need no lock at all.
- Prefer higher-level constructs from
java.util.concurrent(executors, queues) over raw locks; the basics are covered in multithreading in Java.
Pro tip: A single lock cannot deadlock with itself. Before adding a second lock to a class, ask whether one coarser lock is fast enough. Correct-but-slightly-slower beats fast-but-occasionally-frozen in almost every business application.
Deadlock vs livelock vs starvation
These three are frequently confused and frequently asked together.
| Problem | Thread state | CPU usage | Example |
|---|---|---|---|
| Deadlock | BLOCKED forever | None | Two threads, opposite lock order |
| Livelock | RUNNABLE, no progress | High | Two threads endlessly retrying and backing off in sync |
| Starvation | Waiting indefinitely | Normal | Low-priority thread never scheduled; unfair lock always won by others |
A livelock often appears when you fix a deadlock badly: both threads detect contention, release, retry at the same moment, collide again, and loop. Adding a small random backoff before retrying usually resolves it. Starvation is addressed with fair locks (new ReentrantLock(true)) or by not abusing thread priorities.
How this appears in interviews
Deadlock questions show up from the 2-year experience level onward, usually in this sequence: define it, list the four conditions, write or spot a deadlock in code, then explain prevention. The strongest answers name lock ordering first and mention tryLock as the fallback, then volunteer how to confirm a deadlock with jstack.
Interview note: After you explain prevention, many interviewers follow up with: "Your production service is frozen right now — what do you do?" The expected answer is operational: capture a thread dump (or two, a few seconds apart) before restarting, confirm the
Found one Java-level deadlocksection, identify the two code paths, and fix the lock order. Saying "restart the server" without capturing evidence is the trap.
Deadlock is a core topic in the concurrency module of our Java Full Stack course, where you debug a live deadlock with jstack in a lab session rather than just reading about it.
Frequently Asked Questions
What is a deadlock in Java in simple terms?
What are the four conditions required for a deadlock?
How do I detect a deadlock in a running Java application?
Does the JVM automatically recover from a deadlock?
What is the difference between deadlock and livelock?
Can a single thread deadlock itself in Java?
Want to Build Your Career in Java Full Stack with AI?
Join CodeBegun and train with working industry engineers — Explore the Program

