A deadlock is a situation where two or more threads are unable to proceed because each is waiting for the other to release a resource. For a deadlock to occur, four conditions must be met simultaneously:
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 resource 1...");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread 1: Waiting for resource 2...");
synchronized (resource2) {
System.out.println("Thread 1: Holding resource 1 and 2...");
}
}
});
Thread thread2 = new Thread(() -> {
synchronized (resource2) {
System.out.println("Thread 2: Holding resource 2...");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread 2: Waiting for resource 1...");
synchronized (resource1) {
System.out.println("Thread 2: Holding resource 1 and 2...");
}
}
});
thread1.start();
thread2.start();
}
}
In this example, thread1
holds resource1
and waits for resource2
, while thread2
holds resource2
and waits for resource1
, resulting in a deadlock.
One way to prevent deadlocks is to break the circular wait condition. This can be achieved by ordering the resources and ensuring that all threads acquire resources in the same order.
class DeadlockPreventionExample {
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 resource 1...");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread 1: Waiting for resource 2...");
synchronized (resource2) {
System.out.println("Thread 1: Holding resource 1 and 2...");
}
}
});
Thread thread2 = new Thread(() -> {
synchronized (resource1) {
System.out.println("Thread 2: Holding resource 1...");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread 2: Waiting for resource 2...");
synchronized (resource2) {
System.out.println("Thread 2: Holding resource 1 and 2...");
}
}
});
thread1.start();
thread2.start();
}
}
In this code, both threads acquire resource1
first and then resource2
, thus avoiding the circular wait.
You can also prevent deadlocks by avoiding the hold - and - wait condition. One approach is to acquire all necessary resources at once.
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class HoldAndWaitPrevention {
private static final Lock lock1 = new ReentrantLock();
private static final Lock lock2 = new ReentrantLock();
public static void main(String[] args) {
Thread thread = new Thread(() -> {
boolean locked1 = false;
boolean locked2 = false;
try {
while (true) {
locked1 = lock1.tryLock();
if (locked1) {
try {
locked2 = lock2.tryLock();
if (locked2) {
System.out.println("Thread: Holding both locks...");
break;
}
} finally {
if (locked2) {
lock2.unlock();
}
}
}
if (!locked1 ||!locked2) {
if (locked1) {
lock1.unlock();
}
Thread.sleep(100);
}
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if (locked2) {
lock2.unlock();
}
if (locked1) {
lock1.unlock();
}
}
});
thread.start();
}
}
This code tries to acquire both locks at once using tryLock()
, and if it fails, it releases the acquired lock and retries.
If a deadlock occurs in a running Java application, you can use the jstack
tool to generate a thread dump. The thread dump will show the state of all threads, including which threads are involved in the deadlock and what resources they are waiting for.
jstack <pid> > threaddump.txt
Here, <pid>
is the process ID of the Java application. You can then analyze the threaddump.txt
file to identify the deadlock.
Another way to resolve deadlocks is to use timeouts when acquiring locks. If a thread cannot acquire a lock within a specified time, it can release the locks it already holds and try again later.
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class DeadlockResolutionTimeout {
private static final Lock lock1 = new ReentrantLock();
private static final Lock lock2 = new ReentrantLock();
public static void main(String[] args) {
Thread thread = new Thread(() -> {
try {
if (lock1.tryLock()) {
try {
if (lock2.tryLock(100, java.util.concurrent.TimeUnit.MILLISECONDS)) {
try {
System.out.println("Thread: Holding both locks...");
} finally {
lock2.unlock();
}
}
} finally {
lock1.unlock();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
thread.start();
}
}
In this code, the tryLock(long timeout, TimeUnit unit)
method is used to acquire the lock with a timeout.
ExecutorService
, Semaphore
, and CountDownLatch
. These utilities are designed to handle multi - threading scenarios more safely and can help avoid deadlocks.Deadlocks are a complex and challenging issue in Java multi - threaded programming. By understanding the fundamental concepts of deadlocks, using appropriate prevention techniques such as breaking circular wait and avoiding hold - and - wait, and applying common resolution practices like using thread dumps and timeouts, you can effectively prevent and resolve deadlocks in your Java applications. Following best practices will also help you write more robust and reliable multi - threaded code.