Tell about deadlock and how to avoid this problem in programming.
sobes.tech AI
Answer from AI
Deadlock (mutual blocking) is a situation where two or more threads wait for each other indefinitely because each holds a resource needed by the other to continue execution.
To cause a deadlock, four conditions (Coffman conditions) are necessary:
- Mutual Exclusion: Resources cannot be used simultaneously by multiple threads.
- Hold and Wait: A thread holds a resource and waits for additional resources that are being held by other threads.
- No Preemption: Resources cannot be forcibly taken away from a thread; they can only be released voluntarily.
- Circular Wait: There exists a circular chain of threads where each thread waits for a resource held by the next thread in the chain.
Strategies for preventing and avoiding deadlocks:
-
Breaking one of the Coffman conditions:
- Breaking mutual exclusion: Not always possible, but non-blocking algorithms or atomic operations can be used.
- Breaking hold and wait: A thread should request all needed resources at once or release held resources if it cannot acquire all.
- Breaking no preemption: The system can forcibly take resources from a thread under certain conditions.
- Breaking circular wait: Assign a global order for resource acquisition and require all threads to acquire resources in this order.
-
Banker's Algorithm: A dynamic deadlock avoidance method. It systematically analyzes resource requests and grants them only in "safe" states, ensuring that a sequence exists to complete all threads. It is complex to implement and may be inefficient with many resources and threads.
Examples of implementation in Java:
Deadlock example:
// Resource class
class Resource {
private final String name;
public Resource(String name) {
this.name = name;
}
public String getName() {
return name;
}
public synchronized void use() {
System.out.println("Using " + name);
}
}
public class DeadlockExample {
public static void main(String[] args) {
Resource resourceA = new Resource("A");
Resource resourceB = new Resource("B");
// Thread 1: tries to lock A, then B
Thread thread1 = new Thread(() -> {
synchronized (resourceA) {
System.out.println("Thread 1: Locked resource A");
try {
Thread.sleep(100); // Simulate work
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (resourceB) {
System.out.println("Thread 1: Locked resource B");
resourceA.use();
resourceB.use();
}
}
});
// Thread 2: tries to lock B, then A
Thread thread2 = new Thread(() -> {
synchronized (resourceB) {
System.out.println("Thread 2: Locked resource B");
try {
Thread.sleep(100); // Simulate work
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (resourceA) {
System.out.println("Thread 2: Locked resource A");
resourceB.use();
resourceA.use();
}
}
});
thread1.start();
thread2.start();
}
}
Prevention of deadlock (ordered resource acquisition):
// Resource class (unchanged)
class Resource {
private final String name;
public Resource(String name) {
this.name = name;
}
public String getName() {
return name;
}
public synchronized void use() {
System.out.println("Using " + name);
}
}
public class DeadlockPreventionExample {
public static void main(String[] args) {
Resource resourceA = new Resource("A");
Resource resourceB = new Resource("B");
// Thread 1: acquires A, then B (in order A < B)
Thread thread1 = new Thread(() -> {
synchronized (resourceA) {
System.out.println("Thread 1: Locked resource A");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (resourceB) {
System.out.println("Thread 1: Locked resource B");
resourceA.use();
resourceB.use();
}
}
});
// Thread 2: acquires B, then A.
// To avoid deadlock, acquire in the same order (A, then B).
Thread thread2 = new Thread(() -> {
synchronized (resourceA) { // Changed order of acquisition
System.out.println("Thread 2: Locked resource A");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (resourceB) { // Then acquire B
System.out.println("Thread 2: Locked resource B");
resourceA.use();
resourceB.use();
}
}
});
thread1.start();
thread2.start();
}
}
Using high-level abstractions such as java.util.concurrent.locks.ReentrantReadWriteLock or java.util.concurrent.Semaphore, as well as proper multithreaded design, significantly reduces the risk of deadlocks. Methods like tryLock() allow attempting to acquire locks without blocking, helping to avoid waiting.