【14】死锁
【14】死锁
秋雨霏霏 发表于3个月前
【14】死锁
  • 发表于 3个月前
  • 阅读 20
  • 收藏 1
  • 点赞 0
  • 评论 0

腾讯云 新注册用户 域名抢购1元起>>>   

摘要: Jakob Jenkov 并发指南翻译:Thread Deadlock

线程死锁

> 当两个或者多个线程互相阻塞等待对方持有的锁的时候,就会发生死锁。 > 也就是说,线程在申请某个锁的同时,持有者其他线程需要的锁,这通常是由于加锁的顺序不一致而导致的。

> 举例来说,如果线程1已经持有着锁A,然后再申请锁B;同时,线程2已经持有了锁B,然后申请锁A,这样就出现了死锁。 > 线程1永远得不到锁B,而线程2也永远得不到锁A。 > 关键是,两个线程对此还一无所知。 > 两个线程还是会继续持有着已获得的锁并一直等着另一个锁。这就是死锁。

> 两个线程的逻辑,如下所示:

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

> 下面这个例子,展示了一个TreeNode类,这个类上的方法都是synchronized的。

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;
  }
}

> 由于内部还定义了一个parent属性,在synchronized方法中还对parent的方法进行了调用。由于都是TreeNode类型,synchronized实际是对实例的监控器锁的操作。这就会导致当前对象的synchronized方法,调用另一个对象的synchronized方法。 > 这实际上就成了,在持有当前实例的监控器锁的同时,去申请另一个实例的监控器锁。而这种情况,就可能会导致死锁的发生。

> 想象一下,如果线程1在parent实例上调用了parent.addChild(child)方法,同时,线程2在parent的子节点child上调用了child.setParent(parent)方法,那就产生了一个死锁。 > 来分析一下:

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

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

> 首先,线程1调用parent.addChild(child)方法,因为方法是synchronized,所以,线程1也就持有了parent上的锁。

> 然后,线程2调用child.setParent(parent)方法,同样是一个synchronized方法,所以,线程2也就获得了child对象上的锁。

> 这样,parent和child上的锁,就分别被线程1和线程2获得。但是接下来,线程1就会去调用child.setParentOnly(),然而child上的锁已经被线程2获得了,所以线程1只好阻塞等待。 > 同样的,线程2接下来会去调用parent.addChildOnly(),而parent上锁正被线程1所持有,所以线程2也只好阻塞等待。 > 这样就导致了,线程1在等待线程2释放child上的锁;线程2在等待线程1释放parent上的锁。

> 注意:只有在线程1和线程2同时调用,才会死锁。例如,如果线程1稍稍领先于线程2,那就会使得线程1会顺利的获得锁A和B,而线程2会阻塞等待B。这样就不会死锁了。 > 而由于线程的调度是不可预知的,所以无法准确的说死锁会什么时候发生。我们只能说,这样的逻辑会导致死锁。

更复杂的死锁

> 死锁不仅仅是只有两个线程才会发生。实际中,情况会更为复杂,这也是为啥说死锁比较难排查的原因。 > 比如来看看下面这个情况:

Thread 1  locks A, waits for B
Thread 2  locks B, waits for C
Thread 3  locks C, waits for D
Thread 4  locks D, waits for A

> 这就像一个有向图中的圈。如果把线程比作图中顶点,等待另一个线程的锁比作一条边,如果这个有向图中形成了圈,那就意味着死锁。

数据库死锁

> 数据库中的事务,死锁会变得更为复杂。 > 数据库事务可能会包含多条SQL update 请求。 > 当事务中的某条记录被更新时,这条记录可能正被其他事务加锁。这样当前事务就需要等待加锁事务的完成。

> 而一个事务中每一个更新请求都可能需要对某些记录进行加锁处理。 > 这样同时执行的多个事务就可能需要对相同的记录进行更新,这样就会产生死锁风险。 > 例如:

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.

> 由于加锁动作发生于不同的请求,而且当前事务无法预知全部的锁信息,所以,数据库事务发生死锁变得更难发现。


补充几点

可以对死锁的情况,做出一些整理:

  • 锁顺序死锁(lock-ordering deadlock)
    • 两个线程试图通过不同的顺序获得多个相同的锁
    • 注意外部参数引起的顺序变化
    • 致命的拥抱(deadly embrace)
      • 加锁顺序导致持有着对方的锁,并等待对方释放自己需要的锁
    • 环路的依赖关系
    • 持有锁,然后调用外部方法
      • 外部方法不知道会不会在其他对象上加锁
  • 资源死锁(resource deadlock)
    • 资源阻塞
    • 线程饥饿死锁(thread-starvation deadlock)
标签: 并发 死锁 Deadlock
共有 人打赏支持
粉丝 114
博文 75
码字总数 138343
×
秋雨霏霏
如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
* 金额(元)
¥1 ¥5 ¥10 ¥20 其他金额
打赏人
留言
* 支付类型
微信扫码支付
打赏金额:
已支付成功
打赏金额: