Multithreading
Understand the concepts of multithreading and concurrency in Java. Learn how to create and manage threads to perform tasks concurrently.
Deadlock and Livelock in Java Multithreading
Understanding Deadlock
Deadlock is a situation in concurrent programming where two or more threads are blocked forever, waiting for each other to release resources that they need. This occurs when each thread holds a resource that another thread requires, creating a circular dependency.
Conditions for Deadlock (Coffman Conditions)
Deadlock can only occur if all four of the following conditions are met:
- Mutual Exclusion: Resources cannot be used by multiple threads simultaneously. A resource can only be held by one thread at a time.
- Hold and Wait: A thread is holding at least one resource and is waiting to acquire additional resources that are held by other threads.
- No Preemption: A resource can only be released voluntarily by the thread holding it. The operating system cannot forcibly take a resource away from a thread.
- 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.
Example of Deadlock in Java
public class DeadlockExample {
static final Object resource1 = new Object();
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();
}
}
In this example, thread1 acquires resource1
and waits for resource2
, while thread2 acquires resource2
and waits for resource1
. This creates a circular wait condition, resulting in deadlock.
Preventing Deadlock
To prevent deadlock, at least one of the Coffman conditions must be broken.
- Resource Ordering: Impose a strict ordering on resource acquisition. All threads must acquire resources in the same order. This breaks the circular wait condition.
- Timeout: Allow threads to wait for a resource only for a limited time. If the resource is not acquired within the timeout, the thread releases any resources it already holds and tries again later. This breaks the hold and wait condition.
- Resource Allocation Graph: Using a resource allocation graph to detect and prevent cycles can also help.
- Avoid Hold and Wait: Threads should try to acquire all required resources at once, or release held resources before requesting new ones. This can be difficult in practice.
- Preemption (Use with caution): Allowing preemption (OS forcefully taking a resource away) can break the *no preemption* condition but can be tricky to implement in Java's locking mechanisms.
Resolving Deadlock
Resolving deadlock often involves:
- Deadlock Detection and Recovery: Detecting the deadlock and then terminating one or more threads to break the cycle. This is often done by the operating system or a dedicated monitoring tool.
- Restarting threads: After breaking the deadlock, the terminated threads might need to be restarted.
Understanding Livelock
Livelock is another concurrency issue similar to deadlock, but the threads are not blocked. Instead, they repeatedly change their state in response to each other's actions, without making any progress. They are constantly active but getting nowhere.
Example of Livelock in Java
public class LivelockExample {
static class Worker {
private String name;
private boolean hasResource = false;
public Worker(String name) {
this.name = name;
}
public String getName() {
return name;
}
public boolean hasResource() {
return hasResource;
}
public void setResource(boolean hasResource) {
this.hasResource = hasResource;
}
public void tryToShare(Worker other) {
while (hasResource) {
System.out.println(name + ": Giving resource to " + other.getName());
setResource(false);
other.setResource(true);
try { Thread.sleep(10); } catch (InterruptedException e) {} //Simulate work
}
}
}
public static void main(String[] args) {
Worker worker1 = new Worker("Worker 1");
Worker worker2 = new Worker("Worker 2");
worker1.setResource(true);
worker2.setResource(false);
Thread thread1 = new Thread(() -> worker1.tryToShare(worker2));
Thread thread2 = new Thread(() -> worker2.tryToShare(worker1));
thread1.start();
thread2.start();
}
}
In this example, the two workers repeatedly try to give the resource to each other in a polite loop, but neither of them can actually do any real work because they keep passing the resource back and forth. They are alive and active, but stuck.
Preventing Livelock
Livelock can be prevented by introducing randomness or a backoff mechanism into the actions of the threads.
- Random Backoff: Instead of immediately retrying, a thread can wait for a random amount of time before trying again. This breaks the synchronized, repetitive behavior.
- Priority: Assigning priority to one thread can allow that thread to proceed instead of continuously yielding the resource. However, priority-based solutions can lead to other issues.
Resolving Livelock
Similar to prevention, resolving livelock typically involves:
- Introducing Random Delays: Injecting randomness into the retry mechanism can help break the livelock condition.
- Changing Thread Priorities: Temporarily adjusting thread priorities can allow one thread to make progress.
Key Differences between Deadlock and Livelock
- Deadlock: Threads are blocked and waiting forever.
- Livelock: Threads are active but making no progress due to continuous state changes.
- Deadlock: Resources are held indefinitely by waiting threads.
- Livelock: Resources are constantly being passed between threads without any actual work being done.