死锁是两到多个线程被阻塞,等待某些其它处于死锁状态的线程所持有的锁。当多个线程同时但以不同的顺序获取同一组锁时,就可能发生死锁。
死锁示例
如下是死锁情况的一个示例:
如果线程1锁住了A,同时试图去锁住B,而线程2已经锁住了B,同时试图锁住A,这时候死锁就发生了。线程1永远得不到B,而线程2也永远得不到A,并且,二者永远也不会知道发生了这样的事情。它们会永远保持阻塞各自的对象 A 和 B。这种情况就是一个死锁。
这种情况表示如下:
线程 1 锁住 A,等待 B
线程 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
因为锁发生在不同的请求中,并且对于一个事务来说,不可能提前知道所有它需要的锁,所以很难在数据库事务中检测或者防止死锁。