DeadLock
Alright, buckle up! We’ve learned how to create threads, protect shared resources, and make threads communicate. Now, let’s look at one of the most dreaded problems in concurrent programming: Deadlocks.
5. Deadlocks
Concept:
A deadlock occurs when two or more threads are permanently blocked, each waiting for a resource that is held by another thread in the cycle. This results in a complete standstill, and the affected parts of your application become unresponsive, often appearing “frozen.”
- Analogy: Imagine our restaurant has two popular, unique utensils: a Golden Spatula and a Diamond Ladle.
- Scenario: Chef Alice (Thread A) needs both the Golden Spatula and the Diamond Ladle to finish her gourmet dish. Chef Bob (Thread B) also needs both to finish his signature soup.
- Alice acquires the Golden Spatula.
- Bob acquires the Diamond Ladle.
- Alice now tries to acquire the Diamond Ladle, but it’s held by Bob. So, Alice waits.
- Bob now tries to acquire the Golden Spatula, but it’s held by Alice. So, Bob waits.
- Problem: Alice is waiting for Bob, and Bob is waiting for Alice. Neither can proceed, and neither will release what they have. They are permanently deadlocked. The gourmet dish and the signature soup will never be finished!
- Scenario: Chef Alice (Thread A) needs both the Golden Spatula and the Diamond Ladle to finish her gourmet dish. Chef Bob (Thread B) also needs both to finish his signature soup.
Four Conditions for Deadlock (Coffman Conditions):
For a deadlock to occur, all four of these conditions must be met simultaneously:
- Mutual Exclusion: At least one resource must be held in a non-sharable mode. Only one thread can use the resource at a time. (e.g., Only one chef can hold the Golden Spatula).
- Hold and Wait: A thread holding at least one resource is waiting to acquire additional resources held by other threads. (e.g., Alice holds Spatula, waits for Ladle).
- No Preemption: Resources cannot be forcibly taken from a thread; they must be released voluntarily by the thread holding them. (e.g., You can’t just snatch the Spatula from Alice).
- Circular Wait: A set of threads ${T_0, T_1, …, T_n}$ exists such that $T_0$ is waiting for a resource held by $T_1$, $T_1$ is waiting for a resource held by $T_2$, …, $T_n$ is waiting for a resource held by $T_0$. (e.g., Alice waits for Bob, Bob waits for Alice).
To prevent a deadlock, you must break at least one of these four conditions.
“Before” (Problematic Code – The Spatula-Ladle Standoff)
Let’s simulate the two-chef, two-utensil deadlock in Java. We’ll have two threads, and two lock objects representing the resources.
Scenario: Two threads (Thread-A
and Thread-B
) will try to acquire two locks (lock1
and lock2
) in different orders.
Problem: Thread-A
acquires lock1
then lock2
. Thread-B
acquires lock2
then lock1
. If the timing is just right (or wrong!), they can enter a deadlock state.
Java Example (Classic Deadlock):
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class DeadlockDemo {
// Our two "resources" represented by lock objects
private static final Object lock1 = new Object(); // Represents the Golden Spatula
private static final Object lock2 = new Object(); // Represents the Diamond Ladle
static class TaskA implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + ": Trying to acquire lock1 (Spatula)...");
synchronized (lock1) { // Acquire Spatula
System.out.println(Thread.currentThread().getName() + ": Acquired lock1 (Spatula). Now trying to acquire lock2 (Ladle)...");
try {
Thread.sleep(100); // Simulate some work or delay
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
synchronized (lock2) { // Acquire Ladle
System.out.println(Thread.currentThread().getName() + ": Acquired both lock1 and lock2. Performing task.");
} // Release Ladle
} // Release Spatula
System.out.println(Thread.currentThread().getName() + ": Released both locks. Task finished.");
}
}
static class TaskB implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + ": Trying to acquire lock2 (Ladle)...");
synchronized (lock2) { // Acquire Ladle
System.out.println(Thread.currentThread().getName() + ": Acquired lock2 (Ladle). Now trying to acquire lock1 (Spatula)...");
try {
Thread.sleep(100); // Simulate some work or delay
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
synchronized (lock1) { // Acquire Spatula
System.out.println(Thread.currentThread().getName() + ": Acquired both lock2 and lock1. Performing task.");
} // Release Spatula
} // Release Ladle
System.out.println(Thread.currentThread().getName() + ": Released both locks. Task finished.");
}
}
public static void main(String[] args) throws InterruptedException {
System.out.println("--- Deadlock Demonstration ---");
ExecutorService executor = Executors.newFixedThreadPool(2);
executor.submit(new TaskA()); // Thread-A (or pool-1-thread-1)
executor.submit(new TaskB()); // Thread-B (or pool-1-thread-2)
executor.shutdown();
// Wait for a reasonable time to observe deadlock.
// If it deadlocks, this awaitTermination will timeout.
if (!executor.awaitTermination(5, TimeUnit.SECONDS)) {
System.err.println("\n!!! DEADLOCK DETECTED (or timeout occurred) !!!");
System.err.println("Threads are likely blocked indefinitely.");
// You can generate a thread dump here to see their states
// e.g., using jstack <pid> or Ctrl+Break on console (Windows) / Ctrl+\ (Linux/macOS)
} else {
System.out.println("\nAll tasks completed (no deadlock in this run).");
}
}
}
Likely Output (if deadlock occurs):
You’ll see messages indicating each thread acquired one lock, and then tries to acquire the other. The awaitTermination
will likely timeout, and the program will not exit gracefully. The JVM process might keep running even after the main
method completes, indicating blocked threads.
--- Deadlock Demonstration ---
pool-1-thread-1: Trying to acquire lock1 (Spatula)...
pool-1-thread-2: Trying to acquire lock2 (Ladle)...
pool-1-thread-1: Acquired lock1 (Spatula). Now trying to acquire lock2 (Ladle)...
pool-1-thread-2: Acquired lock2 (Ladle). Now trying to acquire lock1 (Spatula)...
!!! DEADLOCK DETECTED (or timeout occurred) !!!
Threads are likely blocked indefinitely.
In this output, pool-1-thread-1
has lock1
and is waiting for lock2
. pool-1-thread-2
has lock2
and is waiting for lock1
. A perfect circular wait.
“After” (Resolved Code – Breaking Deadlock Conditions)
To prevent deadlocks, we must break one or more of the four Coffman conditions. The easiest and most common condition to break in practice is Circular Wait by enforcing a consistent locking order.
Resolution Strategy: Ordered Resource Acquisition (Breaking Circular Wait)
If all threads acquire resources (locks) in the same predefined order, a circular wait cannot form.
- Analogy: We establish a rule: “Whenever you need both the Golden Spatula and the Diamond Ladle, you MUST pick up the Golden Spatula first, then the Diamond Ladle.”
- If Alice takes Spatula, then Ladle.
- If Bob needs both, he also tries for Spatula first.
- If Alice has Spatula, Bob will wait for Spatula. Alice will eventually get Ladle, finish, release both. Then Bob can get Spatula, then Ladle, and proceed. No deadlock.
Java Example (Preventing Deadlock with Consistent Locking Order):
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class NoDeadlockDemo {
private static final Object lock1 = new Object(); // Resource 1 (e.g., Spatula)
private static final Object lock2 = new Object(); // Resource 2 (e.g., Ladle)
// Task A: Acquires locks in order: lock1, then lock2
static class TaskA implements Runnable {
@Override
public void run() {
String threadName = Thread.currentThread().getName();
System.out.println(threadName + ": Trying to acquire lock1 (Spatula)...");
synchronized (lock1) {
System.out.println(threadName + ": Acquired lock1 (Spatula). Now trying to acquire lock2 (Ladle)...");
try {
Thread.sleep(100); // Simulate work
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
synchronized (lock2) { // Acquire lock2 (always after lock1)
System.out.println(threadName + ": Acquired both lock1 and lock2. Performing task.");
}
}
System.out.println(threadName + ": Released both locks. Task finished.");
}
}
// Task B: Also acquires locks in order: lock1, then lock2 (the crucial change!)
static class TaskB implements Runnable {
@Override
public void run() {
String threadName = Thread.currentThread().getName();
System.out.println(threadName + ": Trying to acquire lock1 (Spatula)...");
synchronized (lock1) { // Acquire lock1 (consistent order)
System.out.println(threadName + ": Acquired lock1 (Spatula). Now trying to acquire lock2 (Ladle)...");
try {
Thread.sleep(100); // Simulate work
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
synchronized (lock2) { // Acquire lock2 (always after lock1)
System.out.println(threadName + ": Acquired both lock1 and lock2. Performing task.");
}
}
System.out.println(threadName + ": Released both locks. Task finished.");
}
}
public static void main(String[] args) throws InterruptedException {
System.out.println("--- No Deadlock Demonstration (Consistent Order) ---");
ExecutorService executor = Executors.newFixedThreadPool(2);
executor.submit(new TaskA());
executor.submit(new TaskB());
executor.shutdown();
if (!executor.awaitTermination(5, TimeUnit.SECONDS)) {
System.err.println("!!! Potential issue: Timeout occurred. Check for liveness problems.");
} else {
System.out.println("\nAll tasks completed. No deadlock occurred.");
}
}
}
Expected Output (now consistently completes without deadlock):
--- No Deadlock Demonstration (Consistent Order) ---
pool-1-thread-1: Trying to acquire lock1 (Spatula)...
pool-1-thread-2: Trying to acquire lock1 (Spatula)...
pool-1-thread-1: Acquired lock1 (Spatula). Now trying to acquire lock2 (Ladle)...
pool-1-thread-1: Acquired both lock1 and lock2. Performing task.
pool-1-thread-1: Released both locks. Task finished.
pool-1-thread-2: Acquired lock1 (Spatula). Now trying to acquire lock2 (Ladle)...
pool-1-thread-2: Acquired both lock1 and lock2. Performing task.
pool-1-thread-2: Released both locks. Task finished.
All tasks completed. No deadlock occurred.
Explanation of the Fix:
By making both TaskA
and TaskB
(which are run by pool-1-thread-1
and pool-1-thread-2
respectively) acquire lock1
before lock2
, we eliminate the possibility of a circular wait.
- If
pool-1-thread-1
getslock1
first,pool-1-thread-2
will block immediately when trying to acquirelock1
. pool-1-thread-1
will then successfully acquirelock2
, complete its task, and release both locks.- Only then will
pool-1-thread-2
be able to acquirelock1
, and thenlock2
, proceeding with its task. - The resources are acquired and released sequentially, preventing the circular dependency.
Production-Grade Practice Points for Deadlocks:
- Consistent Locking Order: This is the most practical and common strategy to prevent deadlocks when multiple locks are involved. Define a global (or at least class-wide) order for acquiring locks and stick to it religiously. This can be based on hash codes, memory addresses, or a logical ordering of resources.
- Timeouts on Locks: When using
java.util.concurrent.locks.Lock
(likeReentrantLock
which we’ll cover later) instead ofsynchronized
blocks, you can usetryLock(long timeout, TimeUnit unit)
. This allows a thread to attempt to acquire a lock for a certain duration and give up if it can’t. This doesn’t prevent deadlocks but makes them detectable and recoverable (the thread can log an error and try again later or pursue an alternative path). - Avoid Nested Locks: If possible, minimize the number of times a thread holds one lock while trying to acquire another. This simplifies reasoning about lock order.
- Resource Hierarchies: Design your resource acquisition in a hierarchy. If Resource A is a parent of Resource B, any thread needing both must acquire A before B.
- Deadlock Detection Tools: In production, use tools like
jstack
(command-line utility), VisualVM, or JConsole to take thread dumps. These tools can often identify threads in a deadlock state and show you which locks they are holding and which they are waiting for. This is crucial for diagnosing issues. - Don’t ignore
InterruptedException
: Whensleep()
,wait()
,join()
, andLock.lockInterruptibly()
are used, they can throwInterruptedException
. Handling this properly (e.g., by logging, cleaning up, and exiting the task) is vital for graceful shutdown and preventing threads from getting stuck in unexpected states.
Understanding and preventing deadlocks is a hallmark of robust concurrent programming. It requires careful design of how your threads interact with shared resources.
Next up, we’ll delve into the fascinating world of Mutex vs. Semaphore, fundamental synchronization primitives that offer different ways to control access to resources. Ready?
Let’s dive into understanding the deadlock concept from the provided material and the Java code examples (DeadlockDemo
and NoDeadlockDemo
), focusing on how deadlocks occur, the four Coffman conditions, how the problematic code creates a deadlock, and how the fixed code prevents it. I’ll also explain how this relates to production-grade practices and tie it to the ExecutorService
context from your previous questions, ensuring it’s clear and teachable for your students. The explanation will be concise yet comprehensive, breaking down the concepts step-by-step.
What is a Deadlock?
A deadlock is a situation in concurrent programming where two or more threads are stuck indefinitely, each waiting for a resource (like a lock) held by another thread in a cycle. This causes the threads to freeze, making parts of the application unresponsive.
Analogy (Golden Spatula and Diamond Ladle)
- Scenario: Two chefs, Alice and Bob, need both a Golden Spatula and a Diamond Ladle to complete their dishes.
- Problem:
- Alice takes the Spatula and waits for the Ladle.
- Bob takes the Ladle and waits for the Spatula.
- Neither can proceed, and neither releases their resource, leading to a deadlock.
- This mirrors the code where threads acquire locks in a way that creates a circular dependency.
The Four Coffman Conditions for Deadlock
For a deadlock to occur, all four conditions must be met:
-
Mutual Exclusion:
- A resource (e.g., a lock) can only be held by one thread at a time.
- Example: Only one chef can hold the Spatula (
lock1
) or Ladle (lock2
).
-
Hold and Wait:
- A thread holding one resource waits for another resource held by a different thread.
- Example: Alice holds the Spatula and waits for the Ladle, which Bob holds.
-
No Preemption:
- Resources cannot be forcibly taken; the holding thread must release them voluntarily.
- Example: You can’t snatch the Spatula from Alice; she must release it.
-
Circular Wait:
- A cycle exists where each thread waits for a resource held by the next thread in the cycle.
- Example: Alice waits for Bob’s Ladle, and Bob waits for Alice’s Spatula.
To prevent a deadlock, you must break at least one of these conditions.
DeadlockDemo: How the Deadlock Occurs
The DeadlockDemo
code demonstrates a deadlock using two threads (TaskA
and TaskB
) and two locks (lock1
and lock2
), executed via an ExecutorService
.
Code Breakdown
private static final Object lock1 = new Object(); // Spatula
private static final Object lock2 = new Object(); // Ladle
static class TaskA implements Runnable {
@Override
public void run() {
synchronized (lock1) { // Acquire Spatula
System.out.println(Thread.currentThread().getName() + ": Acquired lock1 (Spatula). Now trying to acquire lock2 (Ladle)...");
try { Thread.sleep(100); } catch (InterruptedException e) { Thread.currentThread().interrupt(); return; }
synchronized (lock2) { // Try to acquire Ladle
System.out.println(Thread.currentThread().getName() + ": Acquired both lock1 and lock2. Performing task.");
}
}
System.out.println(Thread.currentThread().getName() + ": Released both locks. Task finished.");
}
}
static class TaskB implements Runnable {
@Override
public void run() {
synchronized (lock2) { // Acquire Ladle
System.out.println(Thread.currentThread().getName() + ": Acquired lock2 (Ladle). Now trying to acquire lock1 (Spatula)...");
try { Thread.sleep(100); } catch (InterruptedException e) { Thread.currentThread().interrupt(); return; }
synchronized (lock1) { // Try to acquire Spatula
System.out.println(Thread.currentThread().getName() + ": Acquired both lock2 and lock1. Performing task.");
}
}
System.out.println(Thread.currentThread().getName() + ": Released both locks. Task finished.");
}
}
public static void main(String[] args) throws InterruptedException {
ExecutorService executor = Executors.newFixedThreadPool(2);
executor.submit(new TaskA()); // Thread-A
executor.submit(new TaskB()); // Thread-B
executor.shutdown();
if (!executor.awaitTermination(5, TimeUnit.SECONDS)) {
System.err.println("\n!!! DEADLOCK DETECTED (or timeout occurred) !!!");
} else {
System.out.println("\nAll tasks completed (no deadlock in this run).");
}
}
How the Deadlock Happens
-
Execution Flow:
TaskA
(run bypool-1-thread-1
) acquireslock1
(Spatula) and tries to acquirelock2
(Ladle).TaskB
(run bypool-1-thread-2
) acquireslock2
(Ladle) and tries to acquirelock1
(Spatula).- If timing aligns (helped by
Thread.sleep(100)
),TaskA
holdslock1
and waits forlock2
, whileTaskB
holdslock2
and waits forlock1
.
-
Coffman Conditions Met:
- Mutual Exclusion:
synchronized
blocks ensure only one thread can holdlock1
orlock2
. - Hold and Wait:
TaskA
holdslock1
and waits forlock2
;TaskB
holdslock2
and waits forlock1
. - No Preemption: Java’s
synchronized
locks can’t be forcibly taken; threads must release them. - Circular Wait:
TaskA
waits forTaskB
’slock2
, andTaskB
waits forTaskA
’slock1
.
- Mutual Exclusion:
-
Result:
- The threads are stuck indefinitely, causing a deadlock.
- The
executor.awaitTermination(5, TimeUnit.SECONDS)
times out, printing “DEADLOCK DETECTED” because the tasks don’t complete.
-
Output Example:
--- Deadlock Demonstration --- pool-1-thread-1: Trying to acquire lock1 (Spatula)... pool-1-thread-2: Trying to acquire lock2 (Ladle)... pool-1-thread-1: Acquired lock1 (Spatula). Now trying to acquire lock2 (Ladle)... pool-1-thread-2: Acquired lock2 (Ladle). Now trying to acquire lock1 (Spatula)... !!! DEADLOCK DETECTED (or timeout occurred) !!! Threads are likely blocked indefinitely.
NoDeadlockDemo: Preventing the Deadlock
The NoDeadlockDemo
code fixes the deadlock by enforcing a consistent locking order, breaking the Circular Wait condition.
Code Breakdown
static class TaskA implements Runnable {
@Override
public void run() {
String threadName = Thread.currentThread().getName();
synchronized (lock1) { // Acquire Spatula first
System.out.println(threadName + ": Acquired lock1 (Spatula). Now trying to acquire lock2 (Ladle)...");
try { Thread.sleep(100); } catch (InterruptedException e) { Thread.currentThread().interrupt(); return; }
synchronized (lock2) { // Then acquire Ladle
System.out.println(threadName + ": Acquired both lock1 and lock2. Performing task.");
}
}
System.out.println(threadName + ": Released both locks. Task finished.");
}
}
static class TaskB implements Runnable {
@Override
public void run() {
String threadName = Thread.currentThread().getName();
synchronized (lock1) { // Acquire Spatula first (same order as TaskA)
System.out.println(threadName + ": Acquired lock1 (Spatula). Now trying to acquire lock2 (Ladle)...");
try { Thread.sleep(100); } catch (InterruptedException e) { Thread.currentThread().interrupt(); return; }
synchronized (lock2) { // Then acquire Ladle
System.out.println(threadName + ": Acquired both lock1 and lock2. Performing task.");
}
}
System.out.println(threadName + ": Released both locks. Task finished.");
}
}
How the Deadlock is Prevented
-
Consistent Locking Order:
- Both
TaskA
andTaskB
acquirelock1
(Spatula) first, thenlock2
(Ladle). - This eliminates the circular wait because both threads follow the same order, preventing a cycle.
- Both
-
Execution Flow:
- If
pool-1-thread-1
(runningTaskA
) getslock1
,pool-1-thread-2
(runningTaskB
) waits forlock1
. TaskA
acquireslock2
, completes its task, and releases both locks.TaskB
then acquireslock1
, followed bylock2
, and completes.- No thread waits for a resource held by another in a circular fashion.
- If
-
Coffman Condition Broken:
- Circular Wait is eliminated because all threads acquire locks in the same order (
lock1
→lock2
). - This ensures sequential lock acquisition, avoiding deadlocks.
- Circular Wait is eliminated because all threads acquire locks in the same order (
-
Output Example:
--- No Deadlock Demonstration (Consistent Order) --- pool-1-thread-1: Trying to acquire lock1 (Spatula)... pool-1-thread-2: Trying to acquire lock1 (Spatula)... pool-1-thread-1: Acquired lock1 (Spatula). Now trying to acquire lock2 (Ladle)... pool-1-thread-1: Acquired both lock1 and lock2. Performing task. pool-1-thread-1: Released both locks. Task finished. pool-1-thread-2: Acquired lock1 (Spatula). Now trying to acquire lock2 (Ladle)... pool-1-thread-2: Acquired both lock1 and lock2. Performing task. pool-1-thread-2: Released both locks. Task finished. All tasks completed. No deadlock occurred.
Connection to ExecutorService
Both DeadlockDemo
and NoDeadlockDemo
use ExecutorService
to manage threads, which aligns with production-grade practices (as discussed in your WaitNotifyExecutorDemo
question). Here’s how ExecutorService
fits into the deadlock scenario:
-
Thread Management:
Executors.newFixedThreadPool(2)
creates a pool with two threads, ideal for runningTaskA
andTaskB
.ExecutorService
reuses threads, reducing overhead compared to creating rawThread
objects.
-
Task Submission:
executor.submit(new TaskA())
andexecutor.submit(new TaskB())
submitRunnable
tasks, which don’t return values but execute the lock-acquisition logic.- Unlike your
NewApproach
code, which usedCallable
for returning sums, hereRunnable
is sufficient since no results are needed.
-
Deadlock Detection:
- The
awaitTermination(5, TimeUnit.SECONDS)
inDeadlockDemo
detects deadlocks by timing out if tasks don’t complete, indicating threads are blocked. - In
NoDeadlockDemo
, tasks complete, soawaitTermination
succeeds.
- The
-
Production Relevance:
- Using
ExecutorService
is a best practice because it abstracts thread management, supports graceful shutdown (executor.shutdown()
), and allows timeout handling (awaitTermination
). - In production, you’d add logging (e.g., SLF4J) and possibly
executor.shutdownNow()
to force-stop stuck tasks after a timeout.
- Using
Teaching Points for Students
-
Deadlock Concept:
- Use the Spatula-Ladle analogy to make deadlocks relatable.
- Explain the four Coffman conditions with examples from the analogy (e.g., Mutual Exclusion: only one chef can hold the Spatula).
-
Why Deadlocks Happen:
- Show how
DeadlockDemo
creates a circular wait by acquiring locks in different orders (TaskA
:lock1
→lock2
;TaskB
:lock2
→lock1
). - Run the code to demonstrate the “frozen” output and timeout.
- Show how
-
Fixing Deadlocks:
- Highlight that
NoDeadlockDemo
prevents deadlocks by using the same lock order (lock1
→lock2
). - Explain why breaking Circular Wait is effective and practical.
- Highlight that
-
ExecutorService Benefits:
- Teach that
ExecutorService
simplifies thread management compared to rawThread
objects. - Show how
submit
andawaitTermination
help manage tasks and detect issues like deadlocks.
- Teach that
-
Production Practices:
- Discuss the importance of consistent lock ordering in real applications.
- Introduce tools like
jstack
or VisualVM for diagnosing deadlocks in production. - Emphasize proper exception handling (e.g.,
InterruptedException
) andExecutorService
shutdown.
-
Visualizing with a Diagram:
- Consider drawing a sequence diagram (or use the Mermaid one below) to show the lock acquisition order in
DeadlockDemo
(causing a cycle) vs.NoDeadlockDemo
(sequential acquisition).
- Consider drawing a sequence diagram (or use the Mermaid one below) to show the lock acquisition order in
Mermaid Sequence Diagram for Clarity
Here’s a Mermaid sequence diagram to visualize the deadlock in DeadlockDemo
and its prevention in NoDeadlockDemo
.
sequenceDiagram
autonumber
actor User
participant Main as Main Program
participant Executor as ExecutorService
participant Thread1 as pool-1-thread-1
participant Thread2 as pool-1-thread-2
participant Lock1 as lock1 (Spatula)
participant Lock2 as lock2 (Ladle)
%% Deadlock Scenario
User->>Main: Start DeadlockDemo
Main->>Executor: Create newFixedThreadPool(2)
activate Executor
Main->>Executor: Submit TaskA
Main->>Executor: Submit TaskB
Executor->>Thread1: Run TaskA
activate Thread1
Executor->>Thread2: Run TaskB
activate Thread2
Thread1->>Lock1: Acquire lock1
Lock1-->>Thread1: lock1 granted
Thread2->>Lock2: Acquire lock2
Lock2-->>Thread2: lock2 granted
Thread1->>Lock2: Try to acquire lock2
note over Thread1,Lock2: Blocked (lock2 held by Thread2)
Thread2->>Lock1: Try to acquire lock1
note over Thread2,Lock1: Blocked (lock1 held by Thread1)
note over Thread1,Thread2: Deadlock: Circular Wait
Main->>Executor: awaitTermination(5s)
Executor-->>Main: Timeout (tasks blocked)
Main->>Executor: shutdown()
deactivate Executor
%% Divider
note over User,Lock2: --- NoDeadlockDemo (Fixed) ---
User->>Main: Start NoDeadlockDemo
Main->>Executor: Create newFixedThreadPool(2)
activate Executor
Main->>Executor: Submit TaskA
Main->>Executor: Submit TaskB
Executor->>Thread1: Run TaskA
activate Thread1
Executor->>Thread2: Run TaskB
activate Thread2
Thread1->>Lock1: Acquire lock1
Lock1-->>Thread1: lock1 granted
Thread2->>Lock1: Try to acquire lock1
note over Thread2,Lock1: Blocked (lock1 held by Thread1)
Thread1->>Lock2: Acquire lock2
Lock2-->>Thread1: lock2 granted
Thread1->>Thread1: Perform task
Thread1->>Lock2: Release lock2
Thread1->>Lock1: Release lock1
deactivate Thread1
Thread2->>Lock1: Acquire lock1
Lock1-->>Thread2: lock1 granted
Thread2->>Lock2: Acquire lock2
Lock2-->>Thread2: lock2 granted
Thread2->>Thread2: Perform task
Thread2->>Lock2: Release lock2
Thread2->>Lock1: Release lock1
deactivate Thread2
Main->>Executor: awaitTermination(5s)
Executor-->>Main: Tasks completed
Main->>Executor: shutdown()
deactivate Executor
Diagram Explanation
- DeadlockDemo: Shows
Thread1
andThread2
acquiring locks in opposite orders, leading to a circular wait and timeout. - NoDeadlockDemo: Shows both threads acquiring
lock1
thenlock2
, ensuring one thread completes before the other proceeds, avoiding deadlock.
Production-Grade Practices (from the Material)
-
Consistent Locking Order:
- Always acquire locks in a fixed order (e.g.,
lock1
→lock2
) to prevent circular wait. - Example: In a banking app, always lock accounts in order of account ID.
- Always acquire locks in a fixed order (e.g.,
-
Timeouts with
ReentrantLock
:- Use
java.util.concurrent.locks.ReentrantLock
withtryLock(timeout, unit)
to avoid indefinite waiting. - Example:
ReentrantLock lock1 = new ReentrantLock(); ReentrantLock lock2 = new ReentrantLock(); if (lock1.tryLock(1, TimeUnit.SECONDS)) { try { if (lock2.tryLock(1, TimeUnit.SECONDS)) { try { // Perform task } finally { lock2.unlock(); } } } finally { lock1.unlock(); } }
- Use
-
Avoid Nested Locks:
- Minimize holding multiple locks simultaneously to reduce deadlock risk.
- Example: Refactor code to acquire one lock at a time if possible.
-
Resource Hierarchies:
- Define a logical order for resources (e.g., parent → child) to enforce consistent locking.
-
Deadlock Detection Tools:
- Use
jstack
, VisualVM, or JConsole to analyze thread dumps and identify deadlocked threads. - Example: Run
jstack <pid>
to see threads waiting on locks.
- Use
-
Handle
InterruptedException
:- Properly handle interruptions (e.g., restore interrupt status) to ensure graceful shutdown.
- Example:
Thread.currentThread().interrupt()
in catch blocks.
Relating to Your Previous Questions
- ExecutorService: Like
WaitNotifyExecutorDemo
andNewApproach
,ExecutorService
is used here for thread management, showing its versatility for both computation-heavy tasks (summing ranges) and synchronization scenarios (wait/notify, deadlocks). - Callable vs. Runnable:
DeadlockDemo
usesRunnable
since no return value is needed, unlikeNewApproach
(which usedCallable
for sums) orWaitNotifyExecutorDemo
(which usedCallable<Void>
for exception handling). - Wait/Notify vs. Deadlocks: The
WaitNotifyExecutorDemo
usedlock.wait()/notify()
for thread coordination, whileDeadlockDemo
usessynchronized
blocks, but both demonstrate thread synchronization challenges. Deadlocks are a risk when multiple locks are involved, unlike the single-lock wait/notify scenario.
Summary
- Deadlock: Occurs when threads form a circular wait, meeting all four Coffman conditions.
- DeadlockDemo: Demonstrates deadlock by acquiring locks in different orders (
lock1
→lock2
vs.lock2
→lock1
). - NoDeadlockDemo: Prevents deadlock by using a consistent lock order (
lock1
→lock2
), breaking Circular Wait. - ExecutorService: Manages threads efficiently, used in both demos to run tasks, with timeout detection for deadlocks.
- Production Practices: Use consistent lock ordering, timeouts, and tools like
jstack
to prevent and diagnose deadlocks.
For teaching, run both demos to show the deadlock vs. no-deadlock outputs, use the analogy to make it relatable, and demonstrate tools like jstack
for real-world debugging. Let me know if you want:
- A modified version with
ReentrantLock
andtryLock
. - More examples of deadlock scenarios.
- Additional teaching tips or code simplifications!