Explain in detail the causes of deadlock in java and how to avoid it

  • 2021-07-13 05:26:07
  • OfStack

1. Causes of deadlock in Java

In the simplest case of deadlock in Java, one thread T1 holds the lock L1 and claims to acquire the lock L2, while another thread T2 holds the lock L2 and claims to acquire the lock L1. Because the default lock claims are blocked, threads T1 and T2 are blocked forever. Caused a deadlock. This is the easiest and simplest form of deadlock to understand. However, deadlocks in actual environments are often much more complicated than this. There may be multiple threads forming a deadlock loop, such as: thread T1 holds lock L1 and claims to acquire lock L2, thread T2 holds lock L2 and claims to acquire lock L3, and thread T3 holds lock L3 and claims to acquire lock L1, resulting in a lock dependent loop: T1 relies on lock L2 of T2, T2 relies on lock L3 of T3, and T3 relies on lock L1 of T1. Which leads to a deadlock.

From these two examples, we can draw a conclusion that the most fundamental cause of deadlock possibility is that the thread applies for another lock L2 when it obtains one lock L1, that is, the lock L1 wants to include the lock L2, that is to say, it applies for the lock L2 when it obtains the lock L1 and does not release the lock L1, which is the most fundamental cause of deadlock. Another reason is that the default lock application operation is blocked.

2. How to avoid deadlock in Java

Now that we know the cause of deadlock possibility, we can avoid it when coding. Java is an object-oriented programming language. The smallest unit of the program is an object, which encapsulates data and operations. Therefore, the lock 1 in Java is also an object-based unit, and the built-in lock of the object protects the concurrent access of the data in the object. So if we can avoid calling the synchronization methods of other objects in the synchronization methods of objects, we can avoid the possibility of deadlock. There is the possibility of deadlock in the code shown below:


public class ClassB {
  private String address;
  // ...
  
  public synchronized void method1(){
    // do something
  }
  // ... ...
}

public class ClassA {
  private int id;
  private String name;
  private ClassB b;
  // ...
  
  public synchronized void m1(){
    // do something
    b.method1();
  }
  // ... ...
}

The above ClassA. m1 () method calls method1 () of ClassB in the synchronization method of the object, so there is the possibility of deadlock. We can modify the following to avoid deadlock:


public class ClassA {
  private int id;
  private String name;
  private ClassB b;
  // ...
  
  public void m2(){
    synchronized(this){
      // do something
    }
    b.method1();
  }
  // ... ...
}

This reduces the scope of the lock, and the two lock applications do not cross, thus avoiding the possibility of deadlock. This is the most rational situation, because the locks do not cross. But sometimes we are not allowed to do so. At this point, if there is only one m1 method in ClassA, locks on both objects need to be acquired at the same time, and the instance attribute b will not be overflowed (return b; ), but enclose the instance property b in the object, then no deadlock will occur. Because a closed loop of deadlock cannot be formed. However, if there are multiple methods in ClassA that need to acquire locks on two objects at the same time, they must acquire locks in the same order.

For example, in the case of bank transfer, we must obtain locks on two accounts at the same time before we can operate, and the applications for two locks must cross. At this time, we can also break the closed loop of deadlock. In the method of applying for two locks at the same time, we always apply for locks in the same order, for example, we always apply for locks on accounts with large id first, and then apply for locks on accounts with small id, so that the closed loop causing deadlock cannot be formed.


public class Account {
  private int id;  //  Primary key 
  private String name;
  private double balance;
  
  public void transfer(Account from, Account to, double money){
    if(from.getId() > to.getId()){
      synchronized(from){
        synchronized(to){
          // transfer
        }
      }
    }else{
      synchronized(to){
        synchronized(from){
          // transfer
        }
      }
    }
  }

  public int getId() {
    return id;
  }
}

In this way, even if two accounts, such as id=1 and id=100, transfer money to each other, no matter which thread first obtained the lock on id=100, the other thread will not obtain the lock on id=1 (because he did not obtain the lock on id=100), and only which thread first obtained the lock on id=100 will transfer money first. In addition to using id, if there is no attribute like id to compare, you can also use the value of hashCode () of the object for comparison.

As mentioned above, another cause of deadlock is that the default lock application operation is blocked, so if we don't use the default blocked lock, we can avoid deadlock. We can use the ReentrantLock. tryLock () method, and in 1 loop, if tryLock () returns a failure, we release the acquired lock and sleep for 1 short time. This breaks the closed loop of deadlock.

For example, thread T1 holds lock L1 and requests to acquire lock L2, while thread T2 holds lock L2 and requests to acquire lock L3, and thread T3 holds lock L3 and requests to acquire lock L1

At this time, if T3 fails to apply for locking L1, then T3 releases the lock L3 and sleeps, then T2 can obtain L3, and then L2 and L3 are released after T2 is executed, so T1 can also obtain L2 and then release the locks L1 and L2. Then T3 wakes up after sleep and can also obtain L1 and L3. Breaking the closed loop of deadlock.

These situations are relatively easy to deal with, because they are all related, and we can easily realize the possibility of deadlock here, so we can guard against it. In many cases, the scenario is not so obvious that we are aware of the possibility of deadlock. So we should pay attention to:

1 Once we call a method of another object in a synchronization method, or within the scope of a lock, we should be 10 points careful:

1) If this method of other objects takes a long time, it will lead to the lock being held by us for a long time;

2) If this method of other objects is a synchronous method, then attention should be paid to avoid the possibility of deadlock;

Preferably, delay methods and synchronization methods that can avoid calling other objects in one synchronization method. If you can't avoid it, you should take the coding skills mentioned above to break the closed loop of deadlock and prevent the occurrence of deadlock. At the same time, we can try to use "immutable objects" to avoid the use of locks. In some cases, the sharing of objects can also be avoided. For example, new 1 replaces the shared object with a new object, because lock 1 is generally on an object, and the objects are different, so deadlock can be avoided. In addition, static synchronization method can be avoided as much as possible, because static synchronization is equivalent to global lock. There are also a number of closure techniques available, such as stack closure, thread closure, ThreadLocal, which can reduce object sharing and thus reduce the likelihood of deadlocks.

Under summary 1:

Root cause of deadlock

1) Multiple threads involve multiple locks, and these locks intersect, which may lead to a lock-dependent closed loop;

2) The default lock request operation is blocked.

Therefore, to avoid deadlock, we must encounter the situation that locks of multiple objects cross in 1, and carefully examine all methods in the classes of these objects for the possibility of loop that leads to lock dependency. Various methods should be taken to eliminate this possibility.


Related articles: