1. Mutual Exclusion
Only one thread can have exclusive access to a resource at any given time. If a second thread tries to access that resource, it must wait until the first thread releases it.
- The Problem: If resources were sharable (like a Read-Only file), there would be no waiting and no deadlock.
2. Hold and Wait
A thread is already holding at least one resource and is waiting to acquire additional resources that are currently being held by other threads.
- The Problem: The thread doesn’t “give up” what it already has while it waits for the next piece of the puzzle.
3. No Preemption
Resources cannot be forcibly taken away from a thread. A resource can only be released voluntarily by the thread holding it after that thread has completed its task.
- The Problem: The OS or JVM cannot “step in” and snatch a lock away to give it to a starving thread.
4. Circular Wait
A closed chain of threads exists such that each thread holds at least one resource needed by the next thread in the chain.
- Example: Thread A waits for Thread B, Thread B waits for Thread C, and Thread C waits for Thread A.
Comparison: How to Break Each Condition
| Condition | Strategy to Break It |
|---|---|
| Mutual Exclusion | Use non-blocking data structures (e.g., AtomicInteger) instead of locks. |
| Hold and Wait | Force a thread to request all required resources at the very beginning. |
| No Preemption | Use ReentrantLock.tryLock(). If the lock isn’t available, the thread “backs at” and releases its current locks. |
| Circular Wait | Global Lock Ordering: Ensure every thread acquires locks in the exact same order (e.g., always Lock A then Lock B). |
2. The “Golden” Snippet: Breaking Circular Wait
This code shows the Circular Wait in action and the simple fix: Lock Ordering.
public class DeadlockFix {
private Object lock1 = new Object();
private Object lock2 = new Object();
// BAD: Potential Deadlock (Circular Wait)
public void threadOneMethod() {
synchronized (lock1) {
synchronized (lock2) { /* Do work */ }
}
}
// BAD: Acquires locks in opposite order
public void threadTwoMethod() {
synchronized (lock2) {
synchronized (lock1) { /* Do work */ }
}
}
// GOOD: Fixed via Lock Ordering
public void fixedThreadTwoMethod() {
synchronized (lock1) { // Order matches threadOneMethod
synchronized (lock2) { /* Do work */ }
}
}
}
Code Explanation:
- The Flaw: In the “Bad” version,
threadOnegrabslock1andthreadTwograbslock2. They are now stuck forever waiting for each other (Circular Wait). - The Fix: In the “Good” version, we force
threadTwoto grablock1first. IfthreadOnealready haslock1,threadTwowill wait at the first lock instead of grabbing one and “holding and waiting” for the second. - Hierarchy: By establishing a rule that “Lock 1 always comes before Lock 2,” you eliminate the possibility of a circle.
Example Output:
Before Fix (Deadlock):
Thread 1: Acquired Lock 1
Thread 2: Acquired Lock 2
... (System hangs indefinitely)
After Fix (Success):
Thread 1: Acquired Lock 1
Thread 1: Acquired Lock 2
Thread 1: Released both
Thread 2: Acquired Lock 1
Thread 2: Acquired Lock 2
Task Completed!
3. The Gotchas
- The Resource Hierarchy: In complex systems, keeping track of lock order is hard. A common practice is to use the
System.identityHashCode()of objects to determine which one to lock first. - The “All or Nothing” Trap: While breaking “Hold and Wait” by requesting all locks at once sounds good, it often leads to Starvation, where a thread waits forever because it can never get all the keys it needs at the exact same time.