CodeGym /Courses /JAVA 25 SELF /Deadlock: causes, examples, and fixes

Deadlock: causes, examples, and fixes

JAVA 25 SELF
Level 53 , Lesson 0
Available

1. A deep dive into deadlock

Deadlock (“mutual blocking”) is a situation where two or more threads wait for each other forever: each holds some resource and tries to acquire another one that is already held by a neighbouring thread. As a result, no one can proceed, and the program hangs. It’s like two cars on a narrow bridge, nose to nose: until one backs up—nobody moves.

In Java, a deadlock is not an exception (Exception) but a program “freeze”. Threads do not finish and do not crash with an error; they simply wait for each other indefinitely. That’s why such bugs are insidious: they appear only “under certain circumstances”.

The four conditions for a deadlock

  • 1. Mutual exclusion
    A resource can be held by only one thread at a time (for example, an object monitor, a file).
  • 2. Hold and wait
    A thread holds one resource and tries to acquire a second without releasing the first.
  • 3. No preemption
    You cannot forcibly take a resource away from a thread: only the thread itself can release it.
  • 4. Circular wait
    There is a closed chain where each thread is waiting for a resource held by the next thread in the chain.

A deadlock is possible only if ALL these conditions hold simultaneously. Break at least one—and a deadlock cannot occur.

2. A code example of a deadlock

Scenario

  • There are two resources: lock1 and lock2 (plain objects).
  • Thread A first acquires lock1, then tries to acquire lock2.
  • Thread B first acquires lock2, then tries to acquire lock1.

Example (do not try this at home!)

public class DeadlockDemo {
    private static final Object lock1 = new Object();
    private static final Object lock2 = new Object();

    public static void main(String[] args) {
        // Thread 1
        Thread thread1 = new Thread(() -> {
            synchronized (lock1) {
                System.out.println("Thread 1: acquired lock1");
                try { Thread.sleep(100); } catch (InterruptedException ignored) {}
                System.out.println("Thread 1: attempts to acquire lock2");
                synchronized (lock2) {
                    System.out.println("Thread 1: acquired lock2");
                }
            }
        });

        // Thread 2
        Thread thread2 = new Thread(() -> {
            synchronized (lock2) {
                System.out.println("Thread 2: acquired lock2");
                try { Thread.sleep(100); } catch (InterruptedException ignored) {}
                System.out.println("Thread 2: attempts to acquire lock1");
                synchronized (lock1) {
                    System.out.println("Thread 2: acquired lock1");
                }
            }
        });

        thread1.start();
        thread2.start();
    }
}

What happens when you run it?

  • Thread 1 acquires lock1, thread 2 — lock2.
  • Both “sleep” for 100 milliseconds, managing to acquire the first lock.
  • Then each tries to acquire the second lock, which is already held by the other thread.
  • Both threads wait for each other indefinitely—a deadlock occurs.

You’ll see in the console:

Thread 1: acquired lock1
Thread 2: acquired lock2
Thread 1: attempts to acquire lock2
Thread 2: attempts to acquire lock1

Why does a deadlock occur? A detailed breakdown

  • Mutual exclusion: each lock can be held by only one thread.
  • Hold and wait: Thread 1 holds lock1 and waits for lock2; Thread 2 holds lock2 and waits for lock1.
  • No preemption: nobody can steal a lock from the outside—only by exiting the synchronized block.
  • Circular wait: the wait forms a cycle between the two threads.

3. How to avoid and prevent deadlocks

Acquire resources in the same order

The key rule: if multiple threads need to acquire multiple resources—always do it in the same order. For example: first lock1, then lock2—everywhere in the code.

public void doSomething() {
    Object firstLock = lock1;
    Object secondLock = lock2; // single order "lock1 -> lock2"
    synchronized (firstLock) {
        synchronized (secondLock) {
            // Work with both resources
        }
    }
}

With a single order, circular wait is impossible—the deadlock condition is violated.

Use tryLock with a timeout (ReentrantLock)

When it’s not known in advance which resources will be needed, use ReentrantLock and the tryLock method so you don’t wait forever.

import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.TimeUnit;

public class TryLockDemo {
    private static final ReentrantLock lock1 = new ReentrantLock();
    private static final ReentrantLock lock2 = new ReentrantLock();

    public void doWork() {
        try {
            if (lock1.tryLock(100, TimeUnit.MILLISECONDS)) {
                try {
                    if (lock2.tryLock(100, TimeUnit.MILLISECONDS)) {
                        try {
                            // Critical section
                        } finally {
                            lock2.unlock();
                        }
                    } else {
                        System.out.println("Failed to acquire lock2, rolling back");
                    }
                } finally {
                    lock1.unlock();
                }
            } else {
                System.out.println("Failed to acquire lock1, rolling back");
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

Plus: you can roll back and try again later. Minus: the code is more complex, but a deadlock is ruled out.

Minimize how long you hold locks

Hold a lock for as little time as possible. Do only what’s necessary inside synchronized/lock; move everything else outside the critical section.

Avoid nested locking

The fewer nested synchronized/lock blocks, the lower the risk of deadlock. If possible, use a single lock for related resources.

Use ready-made thread-safe structures

The standard library already solves many tasks: collections like ConcurrentHashMap and other classes from java.util.concurrent minimize the risk of deadlock and simplify code.

4. Diagnosing deadlocks

Thread dump and jstack

A thread dump is a snapshot of all thread states in the JVM.

From the console:

jstack <pid>

Where <pid> is the Java process ID (you can obtain it via jps).

In an IDE there is usually a “Thread Dump” button on the debug panel.

In the dump, look for:

  • Statuses BLOCKED or WAITING.
  • Messages like waiting to lock ... and locked ....
  • The JVM phrase: "Found one Java-level deadlock:" — when a deadlock is detected.

An example excerpt of a dump:

"Thread-1":
  waiting to lock monitor 0x000000001e4000, (object 0x7f8a5c00, a java.lang.Object),
  which is held by "Thread-2"
"Thread-2":
  waiting to lock monitor 0x000000001e3000, (object 0x7f8a5c10, a java.lang.Object),
  which is held by "Thread-1"

This is a deadlock: the threads are holding each other.

VisualVM and other tools

  • VisualVM — a free tool for thread analysis and deadlock detection.
  • Java Mission Control / Flight Recorder — advanced monitoring and profiling.

In VisualVM you can inspect the thread tree, their states, and get a deadlock notification.

Table: “How not to end up in a deadlock”

Deadlock cause How to avoid
Acquiring resources in different orders Always adhere to a single resource acquisition order
Nested synchronized Minimize nesting; when possible, use a single lock
Holding a lock for too long Reduce the work done inside the critical section
Using multiple locks at the same time Use tryLock with a timeout and support rollback
Non-thread-safe collections Use concurrent collections (ConcurrentHashMap and others)

5. Common mistakes when dealing with deadlocks

Error No. 1: Acquiring resources in different orders. The most common cause is different threads taking locks in different orders. Even if “it seems faster”, stick to a single order—otherwise circular wait is guaranteed.

Error No. 2: Nested synchronized without necessity. Extra nesting increases the risk of deadlock and complicates the code. Simplify your locking model.

Error No. 3: Ignoring tryLock and timeouts. Locking via synchronized will make a thread wait indefinitely. If you’re not certain, use tryLock with a timeout and rollback logic.

Error No. 4: Long-running code inside a lock. Network requests, I/O, and heavy computations under a lock sharply increase the chance of problems. Move them outside the critical section.

Error No. 5: Not analysing thread dumps. A thread dump is your best friend when hunting deadlocks. Use jstack and analyse the BLOCKED/WAITING states and the “holding/waiting to lock” chains.

1
Task
JAVA 25 SELF, level 53, lesson 0
Locked
Passing the Baton Between Robots: Deadlock Danger 🤖
Passing the Baton Between Robots: Deadlock Danger 🤖
1
Task
JAVA 25 SELF, level 53, lesson 0
Locked
Deadlock Rescue: Smart Engineers with Timeout 🛠️
Deadlock Rescue: Smart Engineers with Timeout 🛠️
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION