简介

当两个以上的运算单元,双方都在等待对方停止运行,以获取系统资源,但是没有一方提前退出时,就称为死锁。在多任务操作系统中,操作系统为了协调不同进程,能否获取系统资源时,为了让系统运作,必须要解决这个问题。

例如,如果线程1锁住了A,然后尝试对B进行加锁,同时线程2已经锁住了B,接着尝试对A进行加锁,这时死锁就发生了。线程1永远得不到B,线程2也永远得不到A,并且它们永远也不会知道发生了这样的事情。为了得到彼此的对象(A和B),它们将永远阻塞下去。这种情况就是一个死锁。

发生的情况如下:

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

这里有一个TreeNode类的例子,它调用了不同实例的synchronized方法:

01    public class TreeNode {
02 TreeNode parent = null;
03 List children = new ArrayList();
04
05 public synchronized void addChild(TreeNode child){
06 if(!this.children.contains(child)) {
07 this.children.add(child);
08 child.setParentOnly(this);
09 }
10 }
11
12 public synchronized void addChildOnly(TreeNode child){
13 if(!this.children.contains(child){
14 this.children.add(child);
15 }
16 }
17
18 public synchronized void setParent(TreeNode parent){
19 this.parent = parent;
20 parent.addChildOnly(this);
21 }
22
23 public synchronized void setParentOnly(TreeNode parent{
24 this.parent = parent;
25 }
26 }

如果线程1调用parent.addChild(child)方法的同时有另外一个线程2调用child.setParent(parent)方法,两个线程中的parent表示的是同一个对象,child亦然,此时就会发生死锁。下面的伪代码说明了这个过程:

Thread 1: parent.addChild(child); //locks parent
--> child.setParentOnly(parent); Thread 2: child.setParent(parent); //locks 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加锁的时候被阻塞,这样死锁就不会发生。因为线程调度通常是不可预测的,因此没有一个办法可以准确预测什么时候死锁会发生,仅仅是可能会发生。

死锁的产生

死锁的四个条件是:

  • 禁止抢占 no preemption - 系统资源不能被强制从一个进程中退出
  • 持有和等待 hold and wait - 一个进程可以在等待时持有系统资源
  • 互斥 mutual exclusion - 只有一个进程能持有一个资源
  • 循环等待 circular waiting - 一系列进程互相持有其他进程所需要的资源

死锁只有在这四个条件同时满足时出现。

死锁的预防

因为独占资源必须以互斥方式进行访问,所以要预防死锁只能从破坏后三个条件下手了。

破坏占有并等待条件:

要破坏这个条件,就要求每个进程必须一次性的请求它们所需要的所有资源,若无法全部获取就等待,直到满足为止,也可以采用事务机制,确保可以回滚,即把获取、释放资源做成原子性的。这个方法实现起来可能会比较困难,因为某些情况下,进程并不能事先直到自己需要哪些资源,也有时候并不需要分配到所有资源就可以运行。

破坏不可剥夺条件:

一个已占有资源的进程若要再申请新的资源,它必须先释放已占有的资源。若随后再需要这些资源,需要重新申请。

破坏循环等待条件:

将系统中所有的资源设置标志位、排序,规定所有的进程申请资源必须以一定的顺序(升序或降序)做操作。

当多个线程需要相同的一些锁,但是按照不同的顺序加锁,死锁就很容易发生。

如果能确保所有的线程都是按照相同的顺序获得锁,那么死锁就不会发生。看下面这个例子:

Thread 1:
lock A
lock B Thread 2:
wait for A
lock C (when A locked) Thread 3:
wait for A
wait for B
wait for C

如果一个线程(比如线程3)需要一些锁,那么它必须按照确定的顺序获取锁。它只有获得了从顺序上排在前面的锁之后,才能获取后面的锁。

例如,线程2和线程3只有在获取了锁A之后才能尝试获取锁C(译者注:获取锁A是获取锁C的必要条件)。因为线程1已经拥有了锁A,所以线程2和3需要一直等到锁A被释放。然后在它们尝试对B或C加锁之前,必须成功地对A加了锁。

按照顺序加锁是一种有效的死锁预防机制。但是,这种方式需要你事先知道所有可能会用到的锁(译者注:并对这些锁做适当的排序),但总有些时候是无法预知的。

死锁的避免

死锁的预防是通过破坏产生条件来阻止死锁的产生,但这种方法破坏了系统的并行性和并发性。
死锁产生的前三个条件是死锁产生的必要条件,也就是说要产生死锁必须具备的条件,而不是存在这3个条件就一定产生死锁,那么只要在逻辑上回避了第四个条件就可以避免死锁。
避免死锁采用的是允许前三个条件存在,但通过合理的资源分配算法来确保永远不会形成环形等待的封闭进程链,从而避免死锁。该方法支持多个进程的并行执行,为了避免死锁,系统动态的确定是否分配一个资源给请求的进程。方法如下:
1.如果一个进程的当前请求的资源会导致死锁,系统拒绝启动该进程;
2.如果一个资源的分配会导致下一步的死锁,系统就拒绝本次的分配;
显然要避免死锁,必须事先知道系统拥有的资源数量及其属性。

死锁的消除

最简单的消除死锁的办法是重启系统。更好的办法是终止一个进程的运行。

同样也可以把一个或多个进程回滚到先前的某个状态。如果一个进程被多次回滚,迟迟不能占用必需的系统资源,可能会导致进程饥饿。

Java并发编程(八)-- 死锁的更多相关文章

  1. java并发编程(八) CAS & Unsafe & atomic

    参考文档:https://www.cnblogs.com/xrq730/p/4976007.html CAS(Compare and Swap) 一个CAS方法包含三个参数CAS(V,E,N).V表示 ...

  2. Java并发编程 (八) J.U.C组件拓展

    个人博客网:https://wushaopei.github.io/    (你想要这里多有) 一.J.U.C-FutureTask-1 FutureTask组件,该组件是JUC中的.但该组件不是 A ...

  3. 【Java并发编程】并发编程大合集-值得收藏

    http://blog.csdn.net/ns_code/article/details/17539599这个博主的关于java并发编程系列很不错,值得收藏. 为了方便各位网友学习以及方便自己复习之用 ...

  4. 【Java并发编程】并发编程大合集

    转载自:http://blog.csdn.net/ns_code/article/details/17539599 为了方便各位网友学习以及方便自己复习之用,将Java并发编程系列内容系列内容按照由浅 ...

  5. java并发编程笔记(八)——死锁

    java并发编程笔记(八)--死锁 死锁发生的必要条件 互斥条件 进程对分配到的资源进行排他性的使用,即在一段时间内只能由一个进程使用,如果有其他进程在请求,只能等待. 请求和保持条件 进程已经保持了 ...

  6. Java并发编程原理与实战十一:锁重入&自旋锁&死锁

    一.锁重入 package com.roocon.thread.t6; public class Demo { /* 当第一个线程A拿到当前实例锁后,进入a方法,那么,线程A还能拿到被当前实例所加锁的 ...

  7. java并发编程如何预防死锁

    在java并发编程领域已经有技术大咖总结出了发生死锁的条件,只有四个条件都发生时才会出现死锁: 1.互斥,共享资源X和Y只能被一个线程占用 2.占有且等待,线程T1已经取得共享资源X,在等待共享资源Y ...

  8. Java并发编程实战 04死锁了怎么办?

    Java并发编程文章系列 Java并发编程实战 01并发编程的Bug源头 Java并发编程实战 02Java如何解决可见性和有序性问题 Java并发编程实战 03互斥锁 解决原子性问题 前提 在第三篇 ...

  9. java并发编程工具类JUC第八篇:ConcurrentHashMap

    在之前的文章中已经为大家介绍了java并发编程的工具:BlockingQueue接口.ArrayBlockingQueue.DelayQueue.LinkedBlockingQueue.Priorit ...

  10. 转: 【Java并发编程】之十八:第五篇中volatile意外问题的正确分析解答(含代码)

    转载请注明出处:http://blog.csdn.net/ns_code/article/details/17382679 在<Java并发编程学习笔记之五:volatile变量修饰符-意料之外 ...

随机推荐

  1. poj3162 树形dp|树的直径 + 双单调队列|线段树,好题啊

    题解链接:https://blog.csdn.net/shiqi_614/article/details/8105149 用树形dp是超时的,, /* 先求出每个点可以跑的最长距离dp[i][0|1] ...

  2. settings.py常见配置项

    settings.py常见配置项 1. 配置Django_Admin依照中文界面显示 LANGUAGE_CODE = 'zh-hans' 2. 数据库配置(默认使用sqlite3) 1 .默认使用的s ...

  3. 报错ERR_CONNECTION_REFUSED,如何解决(原创)

    当我访问我的一个后天地址的时候,突然出现了ERR_CONNECTION_REFUSED,但是之前是可以访问的. 我先ping了下这个网址,发现是OK的 然后我想可能是80端口有问题,也就是说可能是WE ...

  4. linux dig 命令使用方法

    ref:https://www.imooc.com/article/26971?block_id=tuijian_wz dig 命令主要用来从 DNS 域名服务器查询主机地址信息. 查询单个域名的 D ...

  5. python---自己实现二分法列表查找

    这是以我自己的思维来实现的,没有用递归. # coding = utf-8 # 二分查找,适用于有序列表,日常编程用不到,因为index函数可以搞定的. # 查找到数字,返回列表中的下标,找不到数字, ...

  6. [转] whistle--全新的跨平台web调试工具

    whistle是基于Node实现的跨平台web调试代理工具,类似的工具有Windows平台上的Fiddler+Willow,基于Java实现的Charles,及公司同事基于Node实现的Livepoo ...

  7. python词云

    词云图 from os import path from PIL import Image import numpy as np import matplotlib.pyplot as plt fro ...

  8. Codeforces 1045C Hyperspace Highways (看题解) 圆方树

    学了一下圆方树, 好神奇的东西呀. #include<bits/stdc++.h> #define LL long long #define fi first #define se sec ...

  9. js清除单选框所选的值

    js清除单选框所选的值 $("input[type='radio']").removeAttr('checked');

  10. Ubuntu + 坚果云

    下载 nautilus_nutstore_amd64.deb sudo dpkg -i nautilus_nutstore_amd64.deb # 运行后,会提示缺少依赖包 sudo apt-get ...