Java Deadlock

In the last lesson you learned about Java synchronization. Locks keep shared data safe, but they hide a trap. When one thread holds key A and wants key B, while another holds key B and wants key A, both wait forever with no error and no crash. This frozen-forever situation is a deadlock, one of the nastiest bugs in multithreading.

🤔 What is a deadlock?

A deadlock is when two or more threads each wait for a lock another one holds, so all of them wait forever and none can move on.

Picture two people sharing two chopsticks. Alex grabs the left and waits for the right. Riya grabs the right and waits for the left. Each holds half of what they need and waits for the other to let go. So they sit there forever and nobody eats.

In Java, the chopsticks are locks and the people are threads. Your program does not crash or print an error. It just stops responding, with no stack trace pointing at the problem. That is what makes deadlock confusing the first time.

🧩 The four conditions for a deadlock

A deadlock can only form when four things are true at once. These are the Coffman conditions. Breaking even one of them stops deadlock from ever happening.

  • Mutual exclusion. A lock can be held by only one thread at a time. That is the whole point of a lock.
  • Hold and wait. A thread holds one lock while waiting to get another lock.
  • No preemption. A lock cannot be taken away by force. The thread that holds it must release it on its own.
  • Circular wait. There is a loop of threads, where each one waits for a lock held by the next one in the loop.

All four must be present together. Most real fixes work by breaking the circular wait, shown soon.

💥 A deadlock you can run

The fastest way to understand deadlock is to build one and watch it hang. The program below makes two locks. The first thread locks A then tries to lock B. The second thread locks B then tries to lock A. The small sleep in the middle makes the deadlock happen almost every time, because each thread grabs its first lock before either reaches for its second.

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("Thread 1: holding lock A...");
sleep(100);
System.out.println("Thread 1: waiting for lock B...");
synchronized (lockB) { // ❌ waits forever
System.out.println("Thread 1: got both locks!");
}
}
});
Thread t2 = new Thread(() -> {
synchronized (lockB) {
System.out.println("Thread 2: holding lock B...");
sleep(100);
System.out.println("Thread 2: waiting for lock A...");
synchronized (lockA) { // ❌ waits forever
System.out.println("Thread 2: got both locks!");
}
}
});
t1.start();
t2.start();
}
static void sleep(long ms) {
try { Thread.sleep(ms); } catch (InterruptedException e) { }
}
}

What happens when you run this:

  • Thread 1 grabs lock A. Thread 2 grabs lock B. Both sleep a moment.
  • Thread 1 wakes and asks for lock B, but Thread 2 holds it, so Thread 1 waits.
  • Thread 2 wakes and asks for lock A, but Thread 1 holds it, so Thread 2 waits.
  • Both are stuck. Each holds what the other needs. The program prints four lines and hangs.

Output

Thread 1: holding lock A...
Thread 2: holding lock B...
Thread 1: waiting for lock B...
Thread 2: waiting for lock A...
(program hangs here forever)

The “got both locks” messages never print, and you have to kill the program by hand. All four Coffman conditions are present: the locks are exclusive, each thread holds one and waits for another, nothing forces a release, and there is a clear circle. That circle is the heart of the problem.

🔍 How to detect a deadlock

When a real program hangs, you need to look inside. The tool is a thread dump, a snapshot of every thread and what each is doing right now.

If you know the process id of the hung program, ask for a thread dump with the jstack tool that ships with the JDK:

jstack <pid>

In the dump, a deadlocked thread shows up in the BLOCKED state, waiting for a lock another thread owns. The JVM even detects the cycle and prints a clear warning. A trimmed version of what jstack reports for our demo:

Output

Found one Java-level deadlock:
=============================
"Thread-0":
waiting to lock monitor (object lockB),
which is held by "Thread-1"
"Thread-1":
waiting to lock monitor (object lockA),
which is held by "Thread-0"
Java stack information for the threads listed above:
"Thread-0":
at DeadlockDemo.lambda$main$0 - waiting to lock <lockB>
- locked <lockA>
"Thread-1":
at DeadlockDemo.lambda$main$1 - waiting to lock <lockA>
- locked <lockB>

This tells the whole story. Thread-0 holds A and waits for B. Thread-1 holds B and waits for A. The Found one Java-level deadlock line is the JVM confirming the cycle. When your app freezes, take a thread dump first. It points straight at the two threads and locks. Tools like VisualVM and JConsole show the same in a friendlier window.

🛠️ Prevent it with consistent lock ordering

The simplest, most reliable fix is to break the circular wait. If every thread grabs the locks in the same order, no cycle can form. You cannot end up with one thread holding A wanting B while another holds B wanting A, because both always take A first.

In the fixed version below, both threads lock A before B. Even Thread 2 grabs A first to keep the order consistent.

public class LockOrderingDemo {
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) { // ✅ A first
synchronized (lockB) { // ✅ then B
System.out.println("Thread 1: got both locks!");
}
}
});
Thread t2 = new Thread(() -> {
synchronized (lockA) { // ✅ A first here too
synchronized (lockB) { // ✅ then B
System.out.println("Thread 2: got both locks!");
}
}
});
t1.start();
t2.start();
}
}

Because both threads ask for A first, only one holds A at a time. Whoever wins A then takes B, does its work, and releases both. The other waits for A, then gets its turn. No cycle, no deadlock. The program finishes cleanly every run.

Output

Thread 1: got both locks!
Thread 2: got both locks!

This is the most important rule. When code needs more than one lock, pick a fixed order and use it everywhere. A common trick is to order by something stable, like a unique id, so any two threads agree on which lock to take first.

⏱️ Prevent it with tryLock and a timeout

A second technique uses a ReentrantLock from java.util.concurrent.locks instead of synchronized. Its tryLock method tries to grab the lock but gives up after a timeout.

This breaks the hold and wait condition. If a thread cannot get the second lock in time, it releases the first, steps back, and retries later. That release is the key. It refuses to sit holding one lock while blocking forever on another.

The example below shows a transfer between two accounts using tryLock with a timeout:

import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.TimeUnit;
public class TryLockDemo {
static ReentrantLock lockA = new ReentrantLock();
static ReentrantLock lockB = new ReentrantLock();
static void transfer(ReentrantLock first, ReentrantLock second, String name) {
try {
if (first.tryLock(1, TimeUnit.SECONDS)) {
try {
if (second.tryLock(1, TimeUnit.SECONDS)) {
try {
System.out.println(name + ": got both locks, working...");
} finally {
second.unlock();
}
} else {
System.out.println(name + ": could not get second lock, backing off");
}
} finally {
first.unlock();
}
} else {
System.out.println(name + ": could not get first lock, backing off");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
public static void main(String[] args) {
new Thread(() -> transfer(lockA, lockB, "Thread 1")).start();
new Thread(() -> transfer(lockB, lockA, "Thread 2")).start();
}
}

Each tryLock(1, TimeUnit.SECONDS) waits at most one second, then returns false instead of blocking forever. When that happens, the thread prints a “backing off” message and unlocks what it holds. So even though Thread 2 asks in the opposite order, the worst case is a short wait and a retry, never a permanent freeze.

Output

Thread 1: got both locks, working...
Thread 2: could not get second lock, backing off

The output varies from run to run, since it depends on timing. But the program always finishes. One thread might back off, but nothing hangs.

Always unlock in a finally block

With ReentrantLock you release the lock yourself by calling unlock. If an exception is thrown while you hold it, you could leak the lock and freeze other threads. Always put the unlock call inside a finally block, so the lock is released no matter what happens. The synchronized keyword does this release for you automatically, which is one reason it is still handy.

🧹 Other ways to lower the risk

Lock ordering and tryLock are the main tools, but a few habits make deadlock far less likely:

  • Reduce lock scope. Hold a lock for as short a time as possible. Do only the small critical section inside the lock, then release it fast. The less time a lock is held, the smaller the window for trouble.
  • Avoid nested locks. A deadlock needs at least two locks held at once. If you can finish your work holding only one lock at a time, deadlock simply cannot happen. Grab the first, finish with it, release it, then grab the next.
  • Do not call unknown code while holding a lock. If you call a method you do not control while holding a lock, that method might try to grab another lock and create a cycle you never planned for.
  • Use higher-level tools. Classes like ConcurrentHashMap and the atomic types handle their own locking carefully, so you avoid juggling raw locks at all.

Deadlock is not the only way threads fail to make progress. Two close cousins:

  • A livelock is when threads keep reacting to each other and never get anything done. Picture two people in a hallway both stepping the same way, never passing. A naive “retry immediately” loop causes this. A small random wait before retrying breaks the pattern.
  • Starvation is when one thread never gets the resource, because greedier threads keep taking it first. It could run if it got a turn. Fair locks like new ReentrantLock(true) hand out the lock in roughly the order threads asked, so no thread waits forever.

Keeping them straight: deadlock is everyone stuck waiting, livelock is everyone busy but going nowhere, and starvation is one thread always last in line.

⚠️ Common Mistakes

The two mistakes below cause most real deadlocks.

  • Inconsistent lock order. Two pieces of code grab the same pair of locks in opposite orders. This is the classic setup for a deadlock and the easiest one to slip into.
// ❌ Wrong: one path takes A then B, another takes B then A
void method1() {
synchronized (lockA) {
synchronized (lockB) { /* ... */ }
}
}
void method2() {
synchronized (lockB) { // opposite order, deadlock waiting to happen
synchronized (lockA) { /* ... */ }
}
}
// ✅ Right: every method takes the locks in the same order
void method1() {
synchronized (lockA) {
synchronized (lockB) { /* ... */ }
}
}
void method2() {
synchronized (lockA) { // same order everywhere
synchronized (lockB) { /* ... */ }
}
}
  • Nesting synchronized blocks without thinking. Each time you put one synchronized block inside another, you are holding two locks at once, which is exactly what deadlock needs. Often you can avoid the nesting entirely.
// ❌ Wrong: holding two locks at the same time for no real reason
synchronized (lockA) {
synchronized (lockB) {
updateBoth();
}
}
// ✅ Right: finish with one lock, release it, then take the next
synchronized (lockA) {
updatePartOne();
}
synchronized (lockB) {
updatePartTwo();
}

The second pattern only works when the two updates do not have to happen together as one unit. When they truly must be done together, keep the nesting but enforce a single, consistent lock order.

✅ Best Practices

Habits that keep deadlock away.

  • Always grab multiple locks in the same global order. This breaks the circular wait and is the most reliable fix.
  • Prefer tryLock with a timeout for code that needs more than one lock, so a thread can back off instead of waiting forever.
  • Hold locks for the shortest time possible. Small critical sections leave less room for trouble.
  • Avoid nested locks when you can finish your work with one lock at a time.
  • Never call unknown or external code while holding a lock, since it might grab another lock behind your back.
  • Always release ReentrantLock in a finally block so an exception cannot leak the lock.
  • Use a fair lock or concurrent collections when starvation is a worry.
  • Take a thread dump first when a program hangs, since it points straight at the deadlocked threads.

🧩 What You’ve Learned

Great work. Let’s recap deadlock.

  • ✅ A deadlock is when two or more threads each hold a lock the other needs, so all of them wait forever and the program hangs with no error.
  • ✅ It needs all four Coffman conditions at once: mutual exclusion, hold and wait, no preemption, and circular wait. Breaking any one of them prevents it.
  • ✅ A classic deadlock is Thread 1 locking A then B while Thread 2 locks B then A.
  • ✅ Detect it with a thread dump using jstack, where deadlocked threads show up as BLOCKED and the JVM prints “Found one Java-level deadlock”.
  • ✅ Prevent it with a consistent global lock order (always take A before B), which removes the circular wait.
  • ✅ Use tryLock with a timeout on a ReentrantLock so a thread backs off instead of waiting forever. Also reduce lock scope and avoid nested locks.
  • ✅ Related problems are livelock (busy but going nowhere) and starvation (one thread never gets a turn).

Check Your Knowledge

Test what you learned. Pick an answer for each question, then click Check.

  1. 1

    What is a deadlock?

    Why: In a deadlock, each thread holds a lock another needs, so none can proceed and the program hangs.

  2. 2

    Which of the four Coffman conditions does consistent lock ordering break?

    Why: If every thread takes locks in the same order, no cycle can form, which removes the circular wait condition.

  3. 3

    What does tryLock with a timeout do to help prevent deadlock?

    Why: tryLock returns false after the timeout, so the thread can release what it holds and retry instead of freezing.

  4. 4

    How can you detect a deadlock in a hung Java program?

    Why: A thread dump shows deadlocked threads as BLOCKED, and the JVM even prints a 'Found one Java-level deadlock' message.

🚀 What’s Next?

Creating and locking threads by hand works, but it is easy to make mistakes like the deadlock you just saw. Java gives you a higher-level way to run many tasks safely using a pool of reusable threads. Let’s learn it.

Java Thread Pools and Executors

Share & Connect