Whenever two or more threads are waiting for one another to finish accessing a resource, a deadlock occurs. This programming tutorial presents a discussion on deadlocks, what causes a thread deadlock, and how programmers can prevent thread deadlock in Java.
Before reading this tutorial, you might want to read our guide: Introduction to Multithreading in Java.
What is a deadlock?
In computing, a deadlock occurs when two or more concurrent operations are waiting for one another to complete. In other words, a deadlock occurs when two threads block each other forever because they are waiting for the other thread to relinquish the lock. Such a situation often arises when two threads share a resource and are both waiting to gain a lock on the shared resource held by the other thread.
The following conditions must be met for a deadlock to occur:
- At least one resource must be mutually exclusive (like a mutex) so that only one thread can access it simultaneously.
- Hold and wait: A thread must hold onto one resource while it waits for another to come along.
- No preemption: You cannot forcibly remove a lock on a resource once it has been acquired by a thread (ie, the lock cannot be preempted).
- Circular wait: Each thread must wait on another for a resource in a circular fashion.
How to Avoid Deadlock in Java
Java provides various methods to avoid thread deadlock, such as using synchronized blocks, using thread-safe collections, and using atomic operations.
Using Thread.join()
Programmers can avoid deadlocks in Java in several ways. For one, you can use the Thread.join() method. You may use Thread.join() to guarantee that one thread finishes before starting another. For example, when one thread is reading from a file, and another is writing to the same file. Thus, a deadlock cannot arise.
Using Synchronization Objects
Deadlock can be avoided by synchronization and using synchronization primitives. Using synchronization objects, like mutexes or semaphores, is another way to prevent deadlock. This safeguards against deadlocks caused by multiple threads vying for a lock on the same resource.
Always ensure that synchronized blocks are used in a fixed order to avoid deadlocks in Java. This means that if multiple threads are trying to access the same resources, they should always obtain locks on the resources in the same sequence. Additionally, it is important to avoid nested synchronized blocks in order to prevent deadlocks.
Avoid nested locks
Developers can also avoid deadlocks by avoiding nested locks, ie, by avoiding acquiring another lock when a lock on an object has already been acquired. You can also avoid deadlock situations by implementing timeout policies for acquiring locks and ensuring that resources are accessed in the same order across different threads.
Avoid Using Locks When Not Needed
Locks should only be acquired when absolutely necessary and should be released as soon as possible. If a thread acquires a lock that it does not need, other threads may be blocked unnecessarily. To avoid unnecessary locks, it is important to understand the resources being accessed by each thread and the locks held by them.
Proper design of code
In addition, you may design your code such that deadlock never happens. Additionally, the application should be designed such that there are no circular wait dependencies among the threads. Use thread-safe classes and data structures to reduce the risk of thread deadlock in Java applications.
When executing multiple tasks, programmers should establish a master task that will carry out a sequence of subtasks in the specified order. This way, we can ensure that no two threads attempt to obtain the same lock simultaneously, preventing any deadlocks from occurring.
Reading: Java Tools for Increased Productivity
Code Example of Deadlock in Java
The following code example illustrates a deadlock situation in Java:
public class MyThreadDeadlockDemo { public static Object lockObjectA = new Object(); public static Object lockObjectB = new Object(); public static void main(String args[]) { MyThreadClassA threadObjectA = new MyThreadClassA(); MyThreadClassB threadObjectB = new MyThreadClassB(); threadObjectA.start(); threadObjectB.start(); } private static class MyThreadClassA extends Thread { public void run() { synchronized(lockObjectA) { System.out.println(“Thread A: Acquired lock A”); try { Thread.sleep(100); } catch (Exception ex) {} System.out.println(“Thread A: Waiting for lock B”); synchronized(lockObjectB) { System.out.println(“Thread A: Acquired lock on A and B”); } } } } private static class MyThreadClassB extends Thread { public void run() { synchronized(lockObjectB) { System.out.println(“Thread B: Acquired lock B”); try { Thread.sleep(100); } catch (Exception ex) {} System.out.println(“Thread B: Waiting for lock A”); synchronized(lockObjectA) { System.out.println(“Thread B: Acquired lock on A and B”); } } } } }
To solve the deadlock problem in the above code example, all you need to do is change the order of the lock in the run method of the MyThreadClassB class, as shown in the code snippet given below:
public void run() { synchronized (lockObjectA) { System.out.println(“Thread B: Acquired lock B”); try { Thread.sleep(100); } catch (Exception ex) {} System.out.println(“Thread B: Waiting for lock A”); synchronized (lockObjectB) { System.out.println(“Thread B: Acquired lock on A and B”); } } }
The complete Java code is given below, for your reference:
public class MyThreadDeadlockDemo { public static Object lockObjectA = new Object(); public static Object lockObjectB = new Object(); public static void main(String args[]) { MyThreadClassA threadObjectA = new MyThreadClassA(); MyThreadClassB threadObjectB = new MyThreadClassB(); threadObjectA.start(); threadObjectB.start(); } private static class MyThreadClassA extends Thread { public void run() { synchronized(lockObjectA) { System.out.println(“Thread A: Acquired lock A”); try { Thread.sleep(100); } catch (Exception ex) {} System.out.println(“Thread A: Waiting for lock B”); synchronized(lockObjectB) { System.out.println(“Thread A: Acquired lock on A and B”); } } } } private static class MyThreadClassB extends Thread { public void run() { synchronized(lockObjectA) { System.out.println(“Thread B: Acquired lock B”); try { Thread.sleep(100); } catch (Exception ex) {} System.out.println(“Thread B: Waiting for lock A”); synchronized(lockObjectB) { System.out.println(“Thread B: Acquired lock on A and B”); } } } } }
Note how the locks have been acquired in a sequence in the run method of the MyThreadClassB class to prevent deadlock.
Final Thoughts on Preventing Deadlocks in Java
Thread deadlock is a major problem that can cause your Java programs to freeze up and become unresponsive. However, developers can follow the best practices outlined in this tutorial to avoid deadlocks. You should also monitor your application for threads that may be waiting too long and take steps to identify potential deadlocks.
Finally, to ensure you are performing threading properly in your Java applications, we recommend you check out our tutorial: Best Practices for Multithreading in Java.