In multithreaded programming, threads often share resources. While locks protect data from being corrupted, they can also cause threads to freeze forever if two or more threads get stuck waiting for each other to release locks. This frozen state is called a deadlock.

In this guide, we will break down how deadlocks occur in Java using a simple coloring-contest analogy and trace the execution path of a deadlock program.

Illustration of a deadlock scenario between two threads waiting on locks
Real-World Analogy: The Stubborn Coloring Contest

Imagine two kids, T1 and T2, sitting at a table participating in a coloring contest. To color their drawings, they both need exactly two tools: a Blue Crayon (Lock 1) and a Red Pencil (Lock 2).

They start coloring at the exact same moment:

  • Kid T1 grabs the Blue Crayon first.
  • Kid T2 grabs the Red Pencil first.
  • Now, Kid T1 reaches for the Red Pencil, but sees Kid T2 is holding it. T1 decides to wait until T2 puts it down.
  • At the same time, Kid T2 reaches for the Blue Crayon, but sees Kid T1 is holding it. T2 decides to wait until T1 puts it down.

Because both kids are stubborn and refuse to share their current tool until they finish coloring, they sit there staring at each other, waiting forever. No drawing gets colored—this is a **deadlock**!

Walkthrough of the Main Method Scenario

Let's trace how the program executes step-by-step from the entry point:

Step 1: Spawning the Stubborn Threads

The main thread instantiates the lock manager and starts two competitor threads:

DeadlockChecker deadlock = new DeadlockChecker();
new Thread(deadlock::operation1, "T1").start();
new Thread(deadlock::operation2, "T2").start();

Thread T1 is configured to run operation1(), and Thread T2 runs operation2().

Step 2: Acquiring the First Lock and Sleeping

Here is what happens in the first fraction of a second:

  • T1 executes `lock1.lock()` and successfully takes Lock 1. It logs: "lock1 acquired, waiting to acquire lock2."
  • T2 executes `lock2.lock()` and successfully takes Lock 2. It logs: "lock2 acquired, waiting to acquire lock1."
  • Both threads then execute `sleep(50)`. This sleep is crucial because it gives the other thread enough time to capture its first lock, guaranteeing they both reach their next locking statement at the same time.
Step 3: Stalling Indefinitely

After waking up from the sleep:

  • T1 attempts to execute `lock2.lock()`. Since `T2` currently holds Lock 2, `T1` freezes and goes to sleep waiting for it.
  • T2 attempts to execute `lock1.lock()`. Since `T1` currently holds Lock 1, `T2` freezes and goes to sleep waiting for it.

Both threads are now blocked, waiting for the lock held by the other. The program is deadlocked and will remain frozen until manually terminated.

Java Implementation

Below is the complete Java code demonstrating a deadlock using explicit ReentrantLock objects:

package io.practise.accolite;
 
import java.util.concurrent.locks.*;
import static java.lang.Thread.sleep;
 
public class DeadlockChecker {
 
    private Lock lock1 = new ReentrantLock();
    private Lock lock2 = new ReentrantLock();
 
    public static void main(String[] args) {
        DeadlockChecker deadlock = new DeadlockChecker();
 
        try {
            new Thread(deadlock::operation1, "T1").start();
            new Thread(deadlock::operation2, "T2").start();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
 
    public void operation1() {
        lock1.lock();
        print("lock1 acquired, waiting to acquire lock2.");
        try {
            sleep(50);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
 
        lock2.lock();
        print("lock2 acquired");
        print("executing first operation.");
 
        lock2.unlock();
        lock1.unlock();
    }
 
    private void print(String s) {
        System.out.println(s);
    }
 
    public void operation2() {
        lock2.lock();
        print("lock2 acquired, waiting to acquire lock1.");
        try {
            sleep(50);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
 
        lock1.lock();
        print("lock1 acquired");
        print("executing second operation.");
 
        lock1.unlock();
        lock2.unlock();
    }
}

Conclusion

Deadlocks occur when locks are acquired in different orders across multiple threads. The easiest way to prevent them is by establishing a strict lock acquisition order (e.g., both threads must acquire Lock 1 first, then Lock 2), or by using timed locking mechanisms like tryLock(), which release resources if a lock cannot be obtained within a set timeframe.