A deadlock in Java occurs when two or more threads are blocked forever, waiting for each other to release the resources they need. Deadlock happens when threads acquire locks in an inconsistent order and attempt to access resources in such a way that they block each other.
In this tutorial, we'll learn what deadlock is, how it can occur in multithreaded Java programs, and how to avoid it.
Weโll also provide several code examples to help you understand the problem and its solutions.
1. What is Deadlock?
A deadlock occurs in multithreaded applications when two or more threads are waiting indefinitely for a condition that can never be satisfied. Deadlocks arise when the following conditions are met:
Mutual Exclusion: At least two threads require exclusive access to resources.
Hold and Wait: Threads hold resources and wait for additional resources held by other threads.
No Preemption: Resources cannot be forcibly taken away from threads; they must be released voluntarily.
Circular Wait: A circular chain of threads exists where each thread holds a resource that the next thread in the chain needs.
These conditions lead to a situation where all threads involved are stuck, waiting for each other, and none can proceed.
2. Example of Deadlock
Below is an example of a deadlock scenario in Java where two threads (Thread-1 and Thread-2) are waiting for each other to release resources (resource1 and resource2).
Example:
public class DeadlockExample { // Resource 1 and Resource 2 private final Object resource1 = new Object(); private final Object resource2 = new Object(); public static void main(String[] args) { DeadlockExample deadlock = new DeadlockExample(); deadlock.startThreads(); } public void startThreads() { // Thread 1 Thread thread1 = new Thread(() -> { synchronized (resource1) { System.out.println("Thread 1: Locked resource 1"); try { Thread.sleep(100); } catch (InterruptedException e) {} System.out.println("Thread 1: Waiting for resource 2"); synchronized (resource2) { System.out.println("Thread 1: Locked resource 2"); } } }); // Thread 2 Thread thread2 = new Thread(() -> { synchronized (resource2) { System.out.println("Thread 2: Locked resource 2"); try { Thread.sleep(100); } catch (InterruptedException e) {} System.out.println("Thread 2: Waiting for resource 1"); synchronized (resource1) { System.out.println("Thread 2: Locked resource 1"); } } }); // Start both threads thread1.start(); thread2.start(); } }
Explanation:
Thread 1 locks resource1 and then waits for resource2.
Thread 2 locks resource2 and then waits for resource1.
Neither thread can proceed because both are holding a resource the other needs, resulting in a deadlock.
Output:
Thread 1: Locked resource 1 Thread 2: Locked resource 2 Thread 1: Waiting for resource 2 Thread 2: Waiting for resource 1
At this point, both threads are stuck, and the program will not progress because they are each waiting for the other to release the resource.
3. How to Avoid Deadlock
There are several strategies to avoid deadlock in Java:
Lock Ordering: Always acquire locks in a consistent order.
Lock Timeout: Use timeout mechanisms while trying to acquire a lock.
Avoid Holding Locks While Waiting: Avoid holding multiple locks simultaneously.
Deadlock Detection: Implement a deadlock detection mechanism (complex but effective).
4. Solution: Avoiding Deadlock with Lock Ordering
By ensuring that all threads acquire locks in the same order, you can prevent circular wait conditions that lead to deadlocks.
Example:
Solving Deadlock by Lock Ordering
public class DeadlockSolution { // Resource 1 and Resource 2 private final Object resource1 = new Object(); private final Object resource2 = new Object(); public static void main(String[] args) { DeadlockSolution solution = new DeadlockSolution(); solution.startThreads(); } public void startThreads() { // Thread 1 Thread thread1 = new Thread(() -> { synchronized (resource1) { System.out.println("Thread 1: Locked resource 1"); try { Thread.sleep(100); } catch (InterruptedException e) {} System.out.println("Thread 1: Waiting for resource 2"); synchronized (resource2) { System.out.println("Thread 1: Locked resource 2"); } } }); // Thread 2 follows the same lock order Thread thread2 = new Thread(() -> { synchronized (resource1) { System.out.println("Thread 2: Locked resource 1"); try { Thread.sleep(100); } catch (InterruptedException e) {} System.out.println("Thread 2: Waiting for resource 2"); synchronized (resource2) { System.out.println("Thread 2: Locked resource 2"); } } }); // Start both threads thread1.start(); thread2.start(); } }
Explanation:
Both Thread 1 and Thread 2 now acquire resource1 first and then resource2, ensuring that all threads follow the same locking order.
This eliminates the risk of deadlock, as there is no circular wait condition.
Output:
Thread 1: Locked resource 1 Thread 2: Locked resource 1 Thread 1: Waiting for resource 2 Thread 1: Locked resource 2 Thread 2: Waiting for resource 2 Thread 2: Locked resource 2
In this case, Thread 1 acquires both resources and finishes, followed by Thread 2, avoiding deadlock.
5. Solution: Avoiding Deadlock with tryLock() and Timeout
The ReentrantLock class in Java provides a tryLock() method, which tries to acquire the lock and can fail if the lock is not available. This method also allows specifying a timeout to avoid long waits.
Example:
Solving Deadlock with tryLock()
import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class TryLockSolution { private final Lock lock1 = new ReentrantLock(); private final Lock lock2 = new ReentrantLock(); public static void main(String[] args) { TryLockSolution solution = new TryLockSolution(); solution.startThreads(); } public void startThreads() { // Thread 1 Thread thread1 = new Thread(() -> { try { if (lock1.tryLock()) { System.out.println("Thread 1: Locked lock1"); try { Thread.sleep(100); if (lock2.tryLock()) { System.out.println("Thread 1: Locked lock2"); lock2.unlock(); } } finally { lock1.unlock(); } } } catch (InterruptedException e) { e.printStackTrace(); } }); // Thread 2 Thread thread2 = new Thread(() -> { try { if (lock2.tryLock()) { System.out.println("Thread 2: Locked lock2"); try { Thread.sleep(100); if (lock1.tryLock()) { System.out.println("Thread 2: Locked lock1"); lock1.unlock(); } } finally { lock2.unlock(); } } } catch (InterruptedException e) { e.printStackTrace(); } }); // Start both threads thread1.start(); thread2.start(); } }
Explanation:
The tryLock() method is used to attempt to acquire the locks without waiting indefinitely.
If a lock is not acquired, the thread proceeds without getting blocked forever, preventing deadlock.
Output (may vary):
Thread 1: Locked lock1 Thread 2: Locked lock2
In this case, one thread may fail to acquire both locks, but the deadlock is avoided, and the program can still proceed.
6. Deadlock Detection
While deadlock detection is more complex to implement, you can monitor thread states or use third-party tools to detect deadlocks at runtime. Java provides ThreadMXBean in the java.lang.management package to detect deadlocked threads.
Example:
Deadlock Detection Using ThreadMXBean
import java.lang.management.ManagementFactory; import java.lang.management.ThreadMXBean; public class DeadlockDetection { public static void main(String[] args) { DeadlockExample deadlock = new DeadlockExample(); deadlock.startThreads(); // Detect deadlock ThreadMXBean bean = ManagementFactory.getThreadMXBean(); while (true) { long[] threadIds = bean.findDeadlockedThreads(); if (threadIds != null) { System.out.println("Deadlock detected!"); break; } } } }
Explanation:
The ThreadMXBean provides a method findDeadlockedThreads() to check if there are any deadlocked threads.
This code continually monitors for deadlocks and prints a message when a deadlock is detected.
Output (if deadlock occurs):
Deadlock detected!
7. Summary of Deadlock Prevention Techniques
Here are a few strategies to prevent deadlocks in Java:
Lock Ordering: Always acquire locks in a fixed order.
TryLock: Use the tryLock() method from ReentrantLock to attempt acquiring locks with a timeout.
Minimize Lock Scope: Hold locks for the shortest time possible to reduce the chance of deadlock.
Avoid Nested Locks: Avoid acquiring multiple locks if possible.
Deadlock Detection: Use tools or implement detection mechanisms like ThreadMXBean to identify deadlocks in the system.
Conclusion
In this tutorial, we explored the concept of thread deadlock in Java, how it can occur, and different strategies for avoiding it. We covered:
What is a deadlock, and how it happens in a multithreaded program.
A simple example of deadlock where two threads wait for each other to release resources.
Techniques to avoid deadlocks using:
Lock ordering to acquire locks in a consistent order.
tryLock() with timeouts to prevent threads from waiting indefinitely.
Using ThreadMXBean for deadlock detection.
By understanding these strategies, you can prevent deadlocks in your Java applications and build more robust, concurrent systems.