跳到主要内容

16、Java并发编程:死锁(DeadLock,线程死锁,数据库死锁)

线程死锁

死锁是指两个或多个线程被阻塞,等待获得死锁中其他一些线程所持有的锁。当多个线程同时需要相同的锁,但以不同的顺序获取它们时,可能会发生死锁。

例如,如果线程1锁定A并试图锁定B,而线程2已经锁定B并试图锁定A,则会出现死锁。线程1永远得不到B,线程2永远得不到A。此外,他们都不会知道。他们将永远被阻塞在他们各自的对象上,A和B。这种情况就是死锁。

这种情况如下所示:

Thread 1  locks A, waits for B
Thread 2  locks B, waits for A

下面的示例是在不同实例中调用同步方法的TreeNode类:

public class TreeNode {
   
     
 
  TreeNode parent   = null;  
  List     children = new ArrayList();

  public synchronized void addChild(TreeNode child){
   
     
    if(!this.children.contains(child)) {
   
     
      this.children.add(child);
      child.setParentOnly(this);
    }
  }
  
  public synchronized void addChildOnly(TreeNode child){
   
     
    if(!this.children.contains(child){
   
     
      this.children.add(child);
    }
  }
  
  public synchronized void setParent(TreeNode parent){
   
     
    this.parent = parent;
    parent.addChildOnly(this);
  }

  public synchronized void setParentOnly(TreeNode parent){
   
     
    this.parent = parent;
  }
}

如果线程(1)调用parent.addChild(child)方法,另一个线程(2)同时调用child.setParent(parent)方法,在同一parent实例和child实例上,可能会发生死锁。可以用下面的伪代码说明这一点:

Thread 1: parent.addChild(child); //locks parent
          --> child.setParentOnly(parent);

Thread 2: child.setParent(parent); //locks child
          --> parent.addChildOnly()

首先,线程1调用parent.addChild(child)。由于addChild()是同步的,因此线程1实际上锁定了parent对象,使其他线程无法访问。

然后,线程2调用child.setParent(parent)。由于setParent()是同步的,因此线程2实际上锁定了child对象,使其他线程无法访问。

现在child对象和parent对象都被两个不同的线程锁定了。接下来,线程1试图调用child.setParentOnly() 方法,但child对象被线程2锁定,因此该方法就阻塞了。线程2也尝试调用parent.addChildOnly(),但parent对象被线程1锁定,导致线程2在该方法上阻塞。现在,两个线程都被阻塞,等待获得另一个线程持有的锁。

注意:两个线程必须同时调用parent.addChild(child)和child.setParent(parent),就像上面描述的那样,在相同的parent实例和child实例上,才发生死锁。上面的代码可能会在很长一段时间内执行正常,直到突然死锁。

线程真的需要“在同一时刻”锁定。例如,如果线程1比线程2提前一点,从而锁定了A和B,那么线程2在尝试锁定B时将被阻塞。也就不会发生死锁。由于线程调度通常是不可预测的,因此无法预测何时发生死锁。只能知道死锁“会”发生。

更复杂的死锁

死锁还可以包含两个以上的线程。这使得它更难被发现。下面是四个线程死锁的示例:

线程 1  锁定 A, 等待 B
线程 2  锁定 B, 等待 C
线程 3  锁定 C, 等待 D
线程 4  锁定 D, 等待 A

线程1等待线程2,线程2等待线程3,线程3等待线程4,线程4等待线程1.

数据库死锁

发生死锁的一种更复杂的情况是数据库业务。一笔数据库业务可能包含许多SQL更新请求。当一笔数据库业务更新记录时,该记录将被锁定,使其他数据库业务无法更新,直到第一笔业务完成。因此,同一业务中的各个更新请求可能会锁定数据库中的某些记录。

例如:

Transaction 1, request 1, locks record 1 for update
Transaction 2, request 1, locks record 2 for update
Transaction 1, request 2, tries to lock record 2 for update.
Transaction 2, request 2, tries to lock record 1 for update.

由于锁是在不同的请求中获取的,并且无法全部知道业务所需的所有锁,因此很难检测或防止数据库业务中出现死锁。