死锁是两到多个线程被阻塞,等待某些其它处于死锁状态的线程所持有的锁。当多个线程同时但以不同的顺序获取同一组锁时,就可能发生死锁。

死锁示例

如下是死锁情况的一个示例:

如果线程1锁住了A,同时试图去锁住B,而线程2已经锁住了B,同时试图锁住A,这时候死锁就发生了。线程1永远得不到B,而线程2也永远得不到A,并且,二者永远也不会知道发生了这样的事情。它们会永远保持阻塞各自的对象 A 和 B。这种情况就是一个死锁。

这种情况表示如下:

  1. 线程 1 锁住 A,等待 B
  2. 线程 2 锁住 B,等待 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)在同一个 parent 和 child 实例上调用 child.setParent(parent) 方法,死锁就会发生。如下是说明这种情况的伪代码:

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

Thread 2: child.setParent(parent); // 锁住 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 更新请求组成。当在一个事务期间更新一条记录时,这条记录会被锁住,以避免其它事务更新,直到第一个事务完成为止。因此,同一个事务内的每次更新请求都可能会锁住数据库中的一些记录。

如果多个事务同时需要更新相同的记录,那么就有发生死锁的风险。比如:

事务 1, 请求 1, 为更新锁住记录 1
事务 2, 请求 1, 为更新锁住记录 2
事务 1, 请求 2, 为更新试图锁住记录 2
事务 2, 请求 2, 为更新试图锁住记录 1

因为锁发生在不同的请求中,并且对于一个事务来说,不可能提前知道所有它需要的锁,所以很难在数据库事务中检测或者防止死锁。