Understanding Java Thread Deadlock — Example and Explanation

💡 What is a Deadlock in Java?

A deadlock occurs when two or more threads are waiting for each other’s resources (locks), and none of them can proceed.
In other words, each thread holds one lock and waits for another, leading to a permanent waiting state — a deadlock.

Deadlocks are one of the most common multithreading pitfalls in Java and are often used in interviews to test understanding of synchronization and resource contention.


⚙️ Scenario Overview

Let’s consider a simple scenario with two locks (lock1 and lock2) and two threads (Thread1 and Thread2):

  • Thread1 tries to acquire lock1 first and then lock2.
  • Thread2 tries to acquire lock2 first and then lock1.

If Thread1 holds lock1 and Thread2 holds lock2, both threads wait indefinitely for each other to release the other lock.
This is a classic deadlock.


🧩 Code Example — Java Thread Deadlock

public class MyClass {
    public static void main(String args[]) {

        // Create two locks (objects)
        String lock1 = "lock1";
        String lock2 = "lock2";

        // Task 1 tries to acquire lock1 -> lock2
        Runnable task1 = new Runnable() {
            public void run() {
                synchronized (lock1) { 
                    System.out.println("Thread1: Main critical section executed");

                    synchronized (lock2) { 
                        System.out.println("Thread1: Sub critical section executed");
                        try {
                            lock2.notify(); // Wake up thread waiting on lock2
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        };

        // Task 2 tries to acquire lock2 -> lock1
        Runnable task2 = new Runnable() {
            public void run() {
                synchronized (lock2) {
                    System.out.println("Thread2: Main critical section executed");
                    try {
                        lock2.wait(); // Release lock2 and wait
                    } catch (Exception e) {
                        e.printStackTrace();
                    }

                    synchronized (lock1) {
                        System.out.println("Thread2: Sub critical section executed");
                    }
                }
            }
        };

        // Create and start threads
        Thread t1 = new Thread(task1);
        Thread t2 = new Thread(task2);

        t1.start();
        t2.start();
    }
}

🔍 Step-by-Step Explanation

  1. Two locks (lock1 and lock2) are created as shared resources.
  2. Thread1 acquires lock1 and tries to get lock2.
  3. Thread2 acquires lock2 and tries to get lock1.
  4. Both threads now wait forever, each holding one lock and waiting for the other.

This creates a deadlock, where neither thread can proceed, because both are waiting indefinitely.


🧠 Key Concepts Demonstrated

1. Synchronized Blocks

Each thread attempts to acquire an object’s monitor lock before entering a synchronized block.

2. Wait and Notify

  • wait() releases the lock temporarily and moves the thread to the waiting state.
  • notify() wakes up a thread that’s waiting on the same object.

⚠️ Note: In the above code, wait() and notify() are not properly balanced across threads.
This is intentional — it demonstrates how wrong synchronization patterns can cause deadlocks.

3. Thread States in Deadlock

  • Both threads move into a BLOCKED or WAITING state.
  • The JVM thread dump (jstack) would show both threads waiting to acquire locks held by each other.

🧩 How to Detect Deadlocks

Option 1: Using jstack

Run:

jstack <process_id>

You’ll see threads marked as BLOCKED with lines like:

Found one Java-level deadlock:
"Thread-1": waiting to lock monitor 0x00007f...

Option 2: Using IDE tools

IntelliJ IDEA or Eclipse can detect and visualize thread deadlocks during debugging.


✅ How to Avoid Deadlocks

  1. Always acquire locks in the same order across all threads.
    Example: If one thread locks A → B, no other thread should lock B → A.
  2. Use tryLock() (from java.util.concurrent.locks.Lock) with a timeout instead of synchronized.
  3. Keep synchronized blocks small — only protect the necessary code.
  4. Avoid nested locks where possible.

🧾 Summary

ConceptDescription
DeadlockTwo or more threads waiting for each other indefinitely.
CauseCircular lock dependency (Thread1 → lock1 → lock2, Thread2 → lock2 → lock1).
DetectionJVM tools like jstack or IDE thread monitors.
PreventionConsistent lock ordering, using tryLock(), minimizing nested locks.

💬 Final Thought

Deadlocks are not just theoretical problems — they can freeze your production application if not carefully designed.
Understanding lock ordering, synchronization, and concurrency primitives is key to writing safe, scalable multithreaded Java code.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top