4) Multithreading Lesson

Thread Deadlock

13 min to complete · By Ryan Desmond

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.

A diagram showing T1 and T2 that are in a deadlock waiting on resource A and resource B.

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 Thread will 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