文档章节

【14】死锁

秋雨霏霏
 秋雨霏霏
发布于 2017/09/09 17:12
字数 1356
阅读 31
收藏 1

线程死锁

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

举例来说,如果线程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)

© 著作权归作者所有

共有 人打赏支持
秋雨霏霏
粉丝 149
博文 91
码字总数 160620
作品 0
杭州
CTO(技术副总裁)
程序员的自我修养——操作系统篇

目录: 1. 进程的有哪几种状态,状态转换图,及导致转换的事件。 2. 进程与线程的区别。 3. 进程通信的几种方式。 4. 线程同步几种方式。 5. 线程的实现方式. (用户线程与内核线程的区别) 6...

马浩
2014/06/30
0
0
高并发Java(5):JDK并发包1

在高并发Java(2):多线程基础中,我们已经初步提到了基本的线程同步操作。这次要提到的是在并发包中的同步控制工具。 1. 各种同步控制工具的使用 1.1 ReentrantLock ReentrantLock感觉上是...

卯金刀GG
2017/11/02
0
0
xunsearch-1.4.6 中文全文检索,消除内存BUG

经过我们和用户的齐心努力,消除了迄今发现的所有异常退出、死锁、内存泄露等BUG,迫不及待地在今天发布 xunsearch-1.4.6 正式稳定版,以便让大家立即享受最畅快稳定的 xunsearch 搜索。 这是...

hightman
2013/03/27
1K
9
精选30道Java多线程面试题

1、线程和进程的区别 2、实现线程有哪几种方式? 3、线程有哪几种状态?它们之间如何流转的? 4、线程中的start()和run()方法有什么区别? 5、怎么终止一个线程?如何优雅地终止线程? 6、T...

java技术栈
2017/08/13
0
0
JAVA之旅(十四)——静态同步函数的锁是class对象,多线程的单例设计模式,死锁,线程中的通讯以及通讯所带来的安全隐患,等待唤醒机制

JAVA之旅(十四)——静态同步函数的锁是class对象,多线程的单例设计模式,死锁,线程中的通讯以及通讯所带来的安全隐患,等待唤醒机制 JAVA之旅,一路有你,加油! 一.静态同步函数的锁是c...

刘桂林
2016/06/06
0
0

没有更多内容

加载失败,请刷新页面

加载更多

Android WebView制作简易浏览器

最终效果 先创建一个WebView控件,其他的就是通过线性布局在上方加入网址输入框和两个按钮 <WebView android:id="@+id/act_webview_wv" android:layout_width="ma...

lanyu96
3分钟前
0
0
解决MacOS升级系统Sierra到Mojave后git报错

错误信息 升级MacOS Sierra到Mac Mojave后执行git命令报错: xcrun: error: invalid active developer path (/Library/Developer/CommandLineTools), missing xcrun at: /Library/Developer/......

阿dai
4分钟前
0
0
兄弟连区块链教程以太源码分析CMD深入分析(一)

cmd包分析 cmd下面总共有13个子包,除了util包之外,每个子包都有一个主函数,每个主函数的init方法中都定义了该主函数支持的命令,如 geth包下面的: func init() { // Initialize the...

兄弟连区块链入门教程
5分钟前
0
0
Titan Framework MongoDB深入理解1

在TitanFrameWork框架中,已经集成了MongoDB的各个功能,现在我们对框架内部的一些重要类进行分析与解读。 MongoDBConverter 在Titan框架中,比较重要的一个接口就是MongoDBConverter,它是作...

云季科技
10分钟前
0
0
SpringBoot集成Quartz

SpringBoot集成Quartz 什么是Quartz Quartz is a richly featured, open source job scheduling library that can be integrated within virtually any Java application - from the smalle......

Grittan
15分钟前
0
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部