When working with parallel programming, there is the risk of threads locking themselves into an infinite execution blockage, which can be called a deadlock.
Parallel execution is an extremely powerful technique that can significantly improve application performance, and if parallel executions are completely independent and isolated from one another, then the data is also split or read-only. In this case, a developer most likely has nothing to worry about.
However, if computational logic in different threads requires shareable resources and/or synchronization of data, the developer has to be extremely careful to avoid the common cases outlined in the section and also double-check/triple-check logic for possible side effects.
How Does a Deadlock Work in Java?
A deadlock happens when at least two threads are waiting on each other to release a resource lock in order to proceed. It's kind of like two people at a stop sign, both waving at each other to go; they're both waiting on the other person. For the moment, they are deadlocked.
For example, imagine you have two threads, T1 and T2. T1 has successfully locked (or synchronized) resource A and is waiting for resource B to be unlocked.
Meanwhile, T2 successfully locked (or synchronized) resource B and is waiting for resource A to be unlocked.
As a result, both Threads will not be able to proceed and, without any external events, will wait forever. This is not good. The image below shows this situation in action.
Java Deadlock Example
Take a look at the code below to see the situation explained above in action.
public class DeadLock {
public static void main(String[] args) {
// Create two resources
// The example uses Integer, but it could
// be any Object (not a primitive)
Integer resourceA = 0;
Integer resourceB = 1;
// Create two threads that will perform deadlock
// Study the implementation below
// Although they are similar, pay attention to the
// order of the resources they synchronize on
System.out.println("You are about to experience deadlock.");
System.out.println(To exit press 'Control+C'");
// Start the threads, pay attention to the output sequence
new Thread(new A(resourceA, resourceB)).start();
new Thread(new B(resourceA, resourceB)).start();
}
}
class T1 implements Runnable {
// Keep references on resource
private final Integer resourceA;
private final Integer resourceB;
public T1(Integer resourceA, Integer resourceB) {
this.resourceA = resourceA;
this.resourceB = resourceB;
}
@Override
public void run() {
System.out.println("Instance of class A run()");
System.out.println("Instance of class A synchronizing on resourceA");
// Attention!!! Locking the resourceA
// so that no other thread could change it
synchronized (resourceA) {
System.out.println("Instance of class A synchronized on resourceA");
try {
// Sleep symbolises some work that the thread should be doing
System.out.println("Instance of class A doing some work");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Instance of class A synchronizing on resourceB");
// Attention!!! Locking the resourceB
// so that no other thread could change it
synchronized (resourceB) {
// Never happens
System.out.println("Instance of class A synchronized "
+ "on resourceA and resourceB");
}
}
}
}
class T2 implements Runnable {
// Keep references on resource
private final Integer resourceA;
private final Integer resourceB;
public T2(Integer resourceA, Integer resourceB) {
this.resourceA = resourceA;
this.resourceB = resourceB;
}
@Override
public void run() {
System.out.println("Instance of class B run()");
System.out.println("Instance of class B synchronizing on resourceB");
// Attention!!! Locking the resourceB so that
// no other thread could change it
synchronized (resourceB) {
System.out.println("Instance of class B synchronized on resourceB");
try {
// Sleep symbolizes some work that the thread should be doing
System.out.println("Instance of class B doing some work");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Instance of class B synchronizing on resourceA");
// Attention!!! Locking the resourceA so that
// no other thread could change it
synchronized (resourceA) {
// Never happens
System.out.println("Instance of class A synchronized "
+ "on resourceB and resourceA");
}
}
}
}
Do Deadlocks Create Errors
Several aspects of your program are directly affected by the appearance of a deadlock, and these are listed below.
Resources
Each Thread in Java consumes a significant amount of available memory, which is not released until the Thread is done. If a Thread is stuck, then the memory is stuck/lost as well.
Performance
Even though it seems like there is no work happening, it is not true: Each Thread receives a portion of CPU time no matter what. So, deadlocks effectively lead to a waste of resources and decreased performance.
Data loss
If a developer decides to put data-saving logic into the Thread, the information may never be saved.
Troubleshooting
A deadlock happens silently, and there are no errors or any other signs of the problem without rigorous system monitoring.
How to Debug and Prevent a Deadlock
Deadlocks are extremely hard to troubleshoot because to do it properly, a developer needs to repeat/recreate the problem in their local development environment.
The main problem is that production servers are typically much more powerful and possess a higher system load. So, to recreate it locally, a developer has to simulate thousands of requests per second.
Debug a Deadlock
Assume that a deadlock happened on a production system and a developer has access; here is the debugging path in this situation:
- Logs: The locked threads are not moving, so no logs were produced.
- Debugging: The system is running in production mode, so no debugging is available.
- Dump JVM state: The only option available to a developer here would be to dump the JVM state. The state may reveal threads that are stuck and some information about the state of variables in the
Thread. But it has no information on “how you get there”.
Prevent a Deadlock
There is no proven recipe, but the next collection of techniques can help prevent deadlocks.
- If possible, avoid manual multithread orchestration. Java provides several build-in mechanisms like
parallelStream,completableFuture,atomicInteger, etc, - Make sure a thread's code is independent and encapsulated within the
Thread, and don’t share any resources - Make sure the shareable resources are read-only, so no synchronization is required
- Introduce a backup strategy where a
Threadwill wait until a timeout and then roll back its logic from the state before any synchronizations, release resources, and then retry. The timer should have a random factor to avoid the livelock (coming soon) - Another alternative is to use optimistic locking instead of synchronization
Summary What is a Deadlock in Java?
- A deadlock happens when at least two threads are waiting on each other
- Resolving deadlocks can be done by looking at the logs, using debug mode, and dumping the JVM state
Program Components Affected by Deadlocks
- Resources
- Performance
- Data loss
- Troubleshooting
Preventing Deadlocks
- Avoid manual multithread orchestration
- Keep each thread's code independent and encapsulated
- Apply timeouts to threads
- Use optimistic locking instead of synchronization