Understanding Deadlock
Deadlock represents a critical failure state in concurrent programming where two or more threads are blocked forever, each waiting for a resource held by the other. It is not a feature to be utilized, but a severe error condition that must be prevented during system design.
This phenomenon typically arises when nested synchronization blocks are utilized across multiple threads with conflicting resource acquisition orders. If Thread 1 holds Lock A and requests Lock B while Thread 2 holds Lock B and requests Lock A, the application enters a standstill.
Consider the following implementation which simulates a circular wait scenario using two distinct resources:
public class DeadlockDemo {
// Define two shared resources acting as locks
private static final Object resourceOne = new Object();
private static final Object resourceTwo = new Object();
public static void main(String[] args) {
// Thread 1 attempts to lock resources in order: One -> Two
Thread threadA = new Thread(() -> {
synchronized (resourceOne) {
System.out.println("Thread A: Acquired lock on Resource One. Attempting to lock Resource Two...");
// Simulate processing to increase the likelihood of context switch
try { Thread.sleep(50); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
synchronized (resourceTwo) {
System.out.println("Thread A: Acquired lock on Resource Two. Action complete.");
}
}
});
// Thread 2 attempts to lock resources in reverse order: Two -> One
Thread threadB = new Thread(() -> {
synchronized (resourceTwo) {
System.out.println("Thread B: Acquired lock on Resource Two. Attempting to lock Resource One...");
// Simulate processing to increase the likelihood of context switch
try { Thread.sleep(50); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
synchronized (resourceOne) {
System.out.println("Thread B: Acquired lock on Resource One. Action complete.");
}
}
});
threadA.start();
threadB.start();
}
}
When this application executes, it often results in a permanent freeze. The execution flow proceeds as follows:
- Thread A successfully enters the first synchronized block, locking
resourceOne. - Thread B successfully enters its first synchronized block, locking
resourceTwo. - Thread A proceeds to the nested block and attempts to acquire
resourceTwo. However, it is forced to wait because Thread B currently holds it. - Thread B proceeds to its nested block and attempts to acquire
resourceOne. It is forced to wait because Thread A currently holds it.
At this stage, Thread A cannot release resourceOne until it obtains resourceTwo, and Thread B cannot release resourceTwo until it obtains resourceOne. Both threads remain in a BLOCKED state indefinitely. To prevent this issue, developers should ensure that locks are always acquired in a consistent, global order across all threads.