死锁(Deadlock)

更新于 2025-12-28

Jakob Jenkov 2021-02-11

线程死锁

死锁是指两个或多个线程相互阻塞,各自等待获取其他线程所持有的锁。当多个线程需要相同的锁,并且以不同的顺序获取这些锁时,就可能发生死锁。

死锁示例

下面是一个典型的死锁场景:

  • 线程 1 锁定了资源 A,并试图锁定资源 B;
  • 同时,线程 2 已经锁定了资源 B,并试图锁定资源 A。

此时,线程 1 永远无法获得 B,而线程 2 也永远无法获得 A。而且,它们彼此都无法感知对方的存在,将永远阻塞在各自的对象上——这就是死锁。

下图说明了这一情况:

线程 1 锁定 A,等待 B  
线程 2 锁定 B,等待 A

以下是一个 TreeNode 类的示例,它在不同实例上调用同步方法:

public class TreeNode {
    TreeNode parent = null;
    List<TreeNode> 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 实例,那么就可能发生死锁。

伪代码如下所示:

线程 1: parent.addChild(child); // 锁住 parent --> 调用 child.setParentOnly(parent);
线程 2: child.setParent(parent); // 锁住 child --> 调用 parent.addChildOnly();

具体过程如下:

  1. 线程 1 调用 parent.addChild(child)。由于 addChild() 是同步方法,线程 1 实际上锁定了 parent 对象。
  2. 线程 2 调用 child.setParent(parent)。由于 setParent() 是同步方法,线程 2 实际上锁定了 child 对象。
  3. 接着,线程 1 尝试调用 child.setParentOnly(),但 child 已被线程 2 锁定,因此线程 1 被阻塞。
  4. 同时,线程 2 尝试调用 parent.addChildOnly(),但 parent 已被线程 1 锁定,因此线程 2 也被阻塞。

此时,两个线程都在等待对方释放锁,形成死锁。

注意:只有当两个线程几乎同时相同的 parent 和 child 实例执行上述操作时,才会发生死锁。这段代码可能长时间正常运行,直到某次恰好触发死锁条件。

线程调度通常是不可预测的,因此我们无法准确预测死锁何时发生,只能确定它有可能发生。


更复杂的死锁

死锁也可能涉及两个以上的线程,这使得检测更加困难。例如,以下四个线程形成了一个环形死锁:

线程 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 进行更新

由于每个事务在执行过程中逐步获取锁,且事先并不知道所有需要的锁,因此数据库中的死锁很难提前检测或完全避免。