Java > Concurrency and Multithreading > Synchronization and Locks > Deadlock and Starvation
Deadlock Demonstration with Two Threads
This code snippet demonstrates a classic deadlock scenario using two threads and two shared resources. Deadlock occurs when two or more threads are blocked forever, waiting for each other to release the resources that they need.
The Code
This code defines two `Object` instances, `resource1` and `resource2`, which represent shared resources. Two threads, `thread1` and `thread2`, are created. `thread1` first acquires a lock on `resource1`, then attempts to acquire a lock on `resource2`. `thread2` first acquires a lock on `resource2`, then attempts to acquire a lock on `resource1`. The `Thread.sleep(10)` calls are added to increase the likelihood of deadlock by allowing each thread to acquire its first resource before the other thread attempts to access it.
public class DeadlockExample {
private static final Object resource1 = new Object();
private static final Object resource2 = new Object();
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
synchronized (resource1) {
System.out.println("Thread 1: Holding resource1...");
try { Thread.sleep(10); } catch (InterruptedException e) {}
System.out.println("Thread 1: Waiting for resource2...");
synchronized (resource2) {
System.out.println("Thread 1: Acquired resource2.");
}
}
});
Thread thread2 = new Thread(() -> {
synchronized (resource2) {
System.out.println("Thread 2: Holding resource2...");
try { Thread.sleep(10); } catch (InterruptedException e) {}
System.out.println("Thread 2: Waiting for resource1...");
synchronized (resource1) {
System.out.println("Thread 2: Acquired resource1.");
}
}
});
thread1.start();
thread2.start();
}
}
Concepts Behind the Snippet
This snippet illustrates the four necessary conditions for deadlock: 1. Mutual Exclusion: Resources are non-shareable. Only one thread can hold a resource at a time. 2. Hold and Wait: A thread holds a resource while waiting for another. 3. No Preemption: A resource can only be released voluntarily by the thread holding it. 4. Circular Wait: There exists a circular chain of threads such that each thread is waiting for a resource held by the next thread in the chain.
Explanation of the Deadlock
In this example, `thread1` holds `resource1` and waits for `resource2`, while `thread2` holds `resource2` and waits for `resource1`. This creates a circular wait condition, causing a deadlock. Neither thread can proceed because they are both waiting for the other to release its resource. The program will likely freeze.
Real-Life Use Case Section
Deadlocks can occur in database systems when transactions hold locks on rows and tables. For example, transaction A might lock row 1, and transaction B might lock row 2. If transaction A then tries to lock row 2, and transaction B tries to lock row 1, a deadlock occurs. Resource allocation in operating systems can also lead to deadlocks if not carefully managed. Consider a system with printers and scanners, where two processes each require both a printer and a scanner.
Best Practices to Avoid Deadlock
Several strategies can be used to prevent deadlocks: 1. Resource Ordering: Acquire resources in a predefined order. This eliminates the circular wait condition. In the example above, both threads should always acquire `resource1` before `resource2`. 2. Timeout: Set a timeout when trying to acquire a lock. If the lock is not acquired within the timeout period, the thread can release any held resources and try again later. 3. Deadlock Detection and Recovery: Allow deadlocks to occur, but implement a mechanism to detect them and then break the deadlock (e.g., by killing one of the threads involved). 4. Avoid Nested Locks: Minimize the use of nested synchronized blocks. The deeper the nesting, the higher the risk of deadlock.
Interview Tip
When discussing deadlocks in an interview, be prepared to explain the four necessary conditions. You should also be able to discuss common strategies for preventing deadlocks and give examples of situations where deadlocks can occur. Being able to provide concrete code examples demonstrates a strong understanding of the topic.
When to use them
Understanding deadlock scenarios is crucial when designing concurrent applications that share resources. You don't 'use' deadlocks intentionally, you avoid them! Recognizing the potential for deadlock helps you choose appropriate synchronization mechanisms and resource allocation strategies.
Alternatives
Alternatives to synchronized blocks include: 1. ReentrantLock: Provides more flexibility than synchronized blocks, including the ability to set timeouts and interrupt waiting threads. 2. ReadWriteLock: Allows multiple readers to access a resource concurrently, but only allows one writer at a time. 3. Atomic Variables: Provide atomic operations on single variables, which can be used to avoid locks in some cases. 4. Concurrent Collections: Collections like `ConcurrentHashMap` provide thread-safe operations without requiring explicit synchronization.
Pros
Cons
FAQ
-
How can I prevent deadlocks in my Java code?
Use resource ordering, timeouts, or deadlock detection and recovery mechanisms. Avoid nested locks whenever possible. -
What is starvation in the context of concurrency?
Starvation occurs when a thread is perpetually denied access to a resource, even though the resource is repeatedly available. This can happen due to unfair scheduling or priority inversions. -
Is it possible to guarantee deadlock freedom?
While it is challenging to guarantee deadlock freedom in all situations, using techniques like resource ordering and timeouts can significantly reduce the risk of deadlocks.