Monday, July 12, 2021

Deadlock in Java Multi-Threading

In Java which supports multi-threading you may have a scenario where a deadlock happens. In this tutorial you'll learn about the scenarios which may lead to a deadlock and some tips about how to avoid deadlock in Java.

What is deadlock

Deadlock in multi-threading describes a situation where two or more threads are blocked forever, waiting for each other.

To describe it in a simple manner let's assume there are two threads Thread-1 and Thread-2 and two objects obj1 and obj2. Thread-1 already holds a lock on obj1 and for further processing it needs a lock on obj2. At the same time Thread-2 holds a lock on obj2 and wants a lock on obj1. In that kind of scenario both threads will be waiting for each other forever to release lock they are already holding thus creating a deadlock.

Deadlock in Java multi-threading environment may happen in case where-

  • One synchronized method is called from another synchronized method. See example.
  • There are nested synchronized blocks. See example.
Let’s see example code for creation of deadlock in these scenarios–

Java Deadlock scenario- Calling one synchronized method from another

public class DeadLockDemo {
 
 private final String name;
 
 public DeadLockDemo(String name){
  this.name = name;
 }
 public String getName() {
  return this.name;
 }
  
 public synchronized void call(DeadLockDemo caller){
  System.out.println(this.getName() + " has asked to call me " + caller.getName());
  try {
   // Introducing some delay
   Thread.sleep(100);
  } catch (InterruptedException e) {
   // TODO Auto-generated catch block
   e.printStackTrace();
  }
  //Calling another synchronized method
  caller.callMe(this);
 }
 
 public synchronized void callMe(DeadLockDemo caller){
  System.out.println(this.getName() + " has called me " + caller.getName());
 }
 
  public static void main(String[] args) {
    DeadLockDemo caller1 = new DeadLockDemo("caller-1");
    DeadLockDemo caller2 = new DeadLockDemo("caller-2");
    // Thread 1   
    new Thread(new Runnable() {
       public void run() { caller1.call(caller2); }
    }).start();

    //Thread 2  
    new Thread(new Runnable() {
       public void run() { caller2.call(caller1); }
    }).start();
  }
}

Output

caller-1 has asked to call me caller-2
caller-2 has asked to call me caller-1

This code may result in a deadlock for the two created threads, if you are executing this program you'll have to stop the program forcefully.

Here we are creating 2 objects of class DeadLockDemo caller1 and caller2. Then call() method is called with those objects in to separate threads. As you already know that if you are using synchronized instance methods then the thread will have exclusive lock one per instance.

So both threads will enter the call() method one using the lock of caller1 object and another using the lock of caller2 object. Within call() method there is another call to a synchronized method callMe(), notice that the thread which has lock on object caller1 will have to get a lock on object caller2 to call this method. Whereas the thread which has lock on object caller2 will have to get a lock on object caller1 to call this method.

But the lock on object caller1 and caller2 are already held by respective threads causing the threads to wait for each other to release those locks. This will lead to a deadlock and callMe() method will never be called.

Deadlock due to nested synchronized block in Java

class Test{
  private final String name;
  public Test(String name){
    this.name = name;
  }
  public String getName() {
    return this.name;
  }
}

class ThreadA implements Runnable{
  private Test test1;
  private Test test2;
  ThreadA(Test test1, Test test2){
    this.test1 = test1;
    this.test2 = test2;
  }
  @Override
  public void run() {
    synchronized(test1){
      System.out.println("" + test1.getName());
      synchronized(test2){
          System.out.println("Reached here");
      }
    }       
  }
}

class ThreadB implements Runnable{

  private Test test1;
  private Test test2;
  ThreadB(Test test1, Test test2){
    this.test1 = test1;
    this.test2 = test2;
  }
  @Override
  public void run() {
    synchronized(test2){
      System.out.println("" + test2.getName());
      synchronized(test1){
        System.out.println("Reached here");
      }
    }   
  }
}
public class DeadLockDemo1{
  public static void main(String[] args) {
    Test test1 = new Test("Test-1");
    Test test2 = new Test("Test-2");
    Thread t1 = new Thread(new ThreadA(test1, test2));
    Thread t2 = new Thread(new ThreadB(test1, test2));
    t1.start();
    
    t2.start();
  }
}

Output

Test-1
Test-2

As can be seen from the output that both the threads are locked while trying to acquire lock of another object. So "Reached here" never gets printed. Thread t1 will start execution of run method in ThreadA and acquire lock on object test1 and then try to acquire lock on object test2. Meanwhile Thread t2 will start execution of run method in ThreadB and acquire lock on object test2 and then try to acquire lock on object test1. So both threads are trying to acquire a lock which is already held by another thread. Thus causing a deadlock.

How to avoid deadlock in Java

  • Avoiding inconsistent lock ordering - Code given in example 2 shows how, order in which locks are acquired can result in deadlock. Fortunately this kind of deadlock is easy to avoid. If we change the order in such a way that each method acquires both of the locks in the same order, two or more threads executing these methods may not cause deadlock.
    class ThreadA implements Runnable{
      private Test test1;
      private Test test2;
      ThreadA(Test test1, Test test2){
        this.test1 = test1;
        this.test2 = test2;
      }
      @Override
      public void run() {
        synchronized(test1){
          System.out.println("" + test1.getName());
          synchronized(test2){
              System.out.println("Reached here");
          }
        }       
      } 
    }
    
    class ThreadB implements Runnable{
      private Test test1;
      private Test test2;
      ThreadB(Test test1, Test test2){
        this.test1 = test1;
        this.test2 = test2;
      }
      @Override
      public void run() {
        synchronized(test1){
          System.out.println("" + test2.getName());
          synchronized(test2){
            System.out.println("Reached here");
          }
        }   
      }
    }
    

    If you notice in both the threads, order in which locks are acquired, is same now.

  • Try to use synchronized blocks - It is always advisable to use synchronized blocks rather than synchronizing the whole method. Using synchronized block will result in the scope of synchronization reduced which means lock will be held for less time. In example 1 if we use synchronized block rather than synchronizing the whole call() method we may avoid the deadlock.

That's all for this topic Deadlock in Java Multi-Threading. If you have any doubt or any suggestions to make please drop a comment. Thanks!


Related Topics

  1. Race Condition in Java Multi-Threading
  2. Livelock in Java Multi-Threading
  3. Volatile Keyword in Java With Examples
  4. What if run() Method Called Directly Instead of start() Method - Java Multi-Threading
  5. Java Multithreading Interview Questions And Answers

You may also like-

  1. Overview of Exception handling in Java
  2. Difference Between Checked And Unchecked Exceptions in Java
  3. How ArrayList Works Internally in Java
  4. Fail-Fast Vs Fail-Safe Iterator in Java
  5. Constructor chaining in Java
  6. covariant return type in Java
  7. Lambda Expressions in Java 8
  8. Transaction Management in Java-JDBC