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 调用
parent.addChild(child)。由于addChild()是同步方法,线程 1 实际上锁定了parent对象。 - 线程 2 调用
child.setParent(parent)。由于setParent()是同步方法,线程 2 实际上锁定了child对象。 - 接着,线程 1 尝试调用
child.setParentOnly(),但child已被线程 2 锁定,因此线程 1 被阻塞。 - 同时,线程 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 进行更新
由于每个事务在执行过程中逐步获取锁,且事先并不知道所有需要的锁,因此数据库中的死锁很难提前检测或完全避免。