Deadlock in Java
Introduction
In concurrent programming, a Deadlock describes a situation where two or more threads are blocked forever, waiting for each other to release a lock. When a deadlock occurs, the involved threads freeze, and the application's progress comes to a complete halt, often requiring a restart of the JVM to resolve.
Understanding deadlocks, how they occur, and how to prevent them is a critical skill for any Java developer working with multi-threaded applications.
Core Concept: The Coffman Conditions
A deadlock can only occur if all four of the following conditions (known as the Coffman conditions) hold simultaneously in a system:
- Mutual Exclusion: At least one resource must be held in a non-shareable mode; only one thread can use the resource at a time.
- Hold and Wait: A thread is currently holding at least one resource and is requesting additional resources held by other threads.
- No Preemption: Resources cannot be forcibly removed from a thread; they must be released voluntarily by the thread holding them.
- Circular Wait: There exists a set of threads
{T1, T2, ..., Tn}such thatT1is waiting for a resource held byT2,T2is waiting for a resource held byT3, ..., andTnis waiting for a resource held byT1.
To prevent a deadlock, you only need to ensure that at least one of these four conditions cannot occur.
Code Examples
1. Beginner Example: Causing a Deadlock
Here is a classic example of a deadlock using synchronized blocks. Thread 1 locks Resource A and tries to lock Resource B. Thread 2 locks Resource B and tries to lock Resource A.
public class DeadlockExample {
// Two shared resources
private static final Object resourceA = new Object();
private static final Object resourceB = new Object();
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
synchronized (resourceA) {
System.out.println("Thread 1: Locked Resource A");
try { Thread.sleep(100); } catch (InterruptedException e) {}
System.out.println("Thread 1: Waiting for Resource B...");
synchronized (resourceB) {
System.out.println("Thread 1: Locked Resource B");
}
}
});
Thread thread2 = new Thread(() -> {
synchronized (resourceB) {
System.out.println("Thread 2: Locked Resource B");
try { Thread.sleep(100); } catch (InterruptedException e) {}
System.out.println("Thread 2: Waiting for Resource A...");
synchronized (resourceA) {
System.out.println("Thread 2: Locked Resource A");
}
}
});
thread1.start();
thread2.start();
}
}
Output:
Thread 1: Locked Resource A
Thread 2: Locked Resource B
Thread 1: Waiting for Resource B...
Thread 2: Waiting for Resource A...
(Program hangs forever)
2. Intermediate Example: Preventing Deadlock via Lock Ordering
One of the most effective ways to prevent the Circular Wait condition is to impose a strict, global ordering on the acquisition of locks. If all threads must acquire locks in the exact same order, a deadlock cannot happen.
public class DeadlockPreventionExample {
private static final Object resourceA = new Object();
private static final Object resourceB = new Object();
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
// Locks A, then B
synchronized (resourceA) {
System.out.println("Thread 1: Locked Resource A");
try { Thread.sleep(100); } catch (InterruptedException e) {}
synchronized (resourceB) {
System.out.println("Thread 1: Locked Resource B");
}
}
});
Thread thread2 = new Thread(() -> {
// Also locks A, then B (Order is maintained!)
synchronized (resourceA) {
System.out.println("Thread 2: Locked Resource A");
try { Thread.sleep(100); } catch (InterruptedException e) {}
synchronized (resourceB) {
System.out.println("Thread 2: Locked Resource B");
}
}
});
thread1.start();
thread2.start();
}
}
3. Advanced Example: Using ReentrantLock with Timeouts
Instead of using intrinsic locks (synchronized), Java's java.util.concurrent.locks.ReentrantLock provides a tryLock() method with a timeout. This breaks the Hold and Wait condition.
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class TimeoutLockExample {
private static final Lock lockA = new ReentrantLock();
private static final Lock lockB = new ReentrantLock();
public static void attemptOperation() {
try {
// Try to acquire Lock A for 2 seconds
if (lockA.tryLock(2, TimeUnit.SECONDS)) {
try {
System.out.println(Thread.currentThread().getName() + " acquired Lock A");
// Try to acquire Lock B for 2 seconds
if (lockB.tryLock(2, TimeUnit.SECONDS)) {
try {
System.out.println(Thread.currentThread().getName() + " acquired Lock B");
// Do work here
} finally {
lockB.unlock();
}
} else {
System.out.println(Thread.currentThread().getName() + " failed to acquire Lock B, aborting.");
}
} finally {
lockA.unlock();
}
} else {
System.out.println(Thread.currentThread().getName() + " failed to acquire Lock A, aborting.");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
Diagnosing Deadlocks in Production
When an application hangs in production, how do you know if it's a deadlock?
Thread Dumps
A Thread Dump is a snapshot of exactly what every thread is doing at a given moment.
- You can generate a thread dump using the command line tool
jstack <PID>. - The JVM is smart enough to automatically detect intrinsic deadlocks and will print a "Found one Java-level deadlock:" section at the bottom of the
jstackoutput.
JConsole / VisualVM
Java provides GUI profiling tools like JConsole and VisualVM (shipped with the JDK or available standalone). These tools have a "Detect Deadlock" button in the Threads tab that instantly highlights deadlocked threads and the resources they are fighting over.
Summary
- A Deadlock happens when threads are indefinitely blocked waiting for locks held by each other.
- The Coffman Conditions outline the four requirements for a deadlock: Mutual Exclusion, Hold and Wait, No Preemption, and Circular Wait.
- Prevention Techniques:
- Avoid nested locks if possible.
- Implement Lock Ordering (always acquire resources in a strict, consistent order).
- Use
ReentrantLock.tryLock()to implement timeouts and safely back off.
- Use
jstackor VisualVM to detect and analyze deadlocks in a running JVM.