您当前的位置:首页 > 电脑百科 > 程序开发 > 编程百科

多线程之死锁详解

时间:2019-10-15 10:25:50  来源:  作者:

一、什么是死锁

当两个或两个以上的线程在执行过程中,因为争夺资源而造成的一种相互等待的状态,由于存在一种环路的锁依赖关系而永远地等待下去,如果没有外部干涉,他们将永远等待下去,此时的这个状态称之为死锁。

经典的 “哲学家进餐” 问题很好地描述了死锁状况:

5个哲学家去吃中餐,坐在一张圆桌旁,他们有5根筷子(而不是5双),并且每两个人中间放一根筷子,哲学家们要么在思考,要么在进餐,每个人都需要一双筷子才能吃到东西,并在吃完后将筷子放回原处继续思考,有些筷子管理算法 (1) 能够使每个人都能相对及时的吃到东西,但有些算法却可能导致一些或者所有哲学家都"饿死",后一种情况将产生死锁:每个人都拥有其他人需要的资源,同时有等待其他人已经拥有的资源,并且每个人在获取所有需要的资源之前都不会放弃已经拥有的资源。筷子管理算法(1):一个饥饿的科学家会尝试获得两根临近的筷子,但如果其中一根正在被另一个科学家使用,那么他将放弃已经得到的那根筷子,并在等待几分钟之后尝试

死锁:每个人都立即抓住自己左边的筷子,然后等待自己右边的筷子空出来,但同时又不放下已经拿到的筷子,形成一种相互等待的状态。

饥饿:哲学家们都同时想吃饭,同时拿起左手边筷子,但是发现右边没有筷子,于是哲学家又同时放下左手边筷子,然后大家发现又有筷子了,又同时开始拿起左手边筷子,又同时放下,然后反复进行。

还有人问你多线程的死锁?快把这篇「多线程之死锁详解」扔给他

 

在线程A持有锁L并想获得锁M的同时,线程B持有锁M并尝试获得锁L,那么这两个线程将永远地等待下去,这种情况就是死锁形式(或者称为"抱死").

还有人问你多线程的死锁?快把这篇「多线程之死锁详解」扔给他

 

二、死锁的四个必要条件

互斥条件:指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用完释放。

请求和保持条件:指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。

不剥夺条件:指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。

环路等待条件:指在发生死锁时,必然存在一个进程——资源的环形链,即进程集合{A,B,C,···,Z} 中的A正在等待一个B占用的资源;B正在等待C占用的资源,……,Z正在等待已被A占用的资源。

三、死锁实例

/**
 * 死锁类示例
 */
public class DeadLock implements Runnable {
	public int flag = 1;
	//静态对象是类的所有对象共享的
	private static Object o1 = new Object(), o2 = new Object();
	@Override
	 public void run() {
		System.out.println("flag:{}"+flag);
		if (flag == 1) {
			//先锁o1,再对o2加锁,环路等待条件
			synchronized (o1) {
				try {
					Thread.sleep(500);
				}
				catch (Exception e) {
					e.printStackTrace();
				}
				synchronized (o2) {
					System.out.println("1");
				}
			}
		}
		if (flag == 0) {
			//先锁o2,在锁01
			synchronized (o2) {
				try {
					Thread.sleep(500);
				}
				catch (Exception e) {
					e.printStackTrace();
				}
				synchronized (o1) {
					System.out.println("0");
				}
			}
		}
	}
	public static void main(String[] args) {
		DeadLock td1 = new DeadLock();
		DeadLock td2 = new DeadLock();
		td1.flag = 1;
		td2.flag = 0;
		//td1,td2都处于可执行状态,但JVM线程调度先执行哪个线程是不确定的。
		//td2的run()可能在td1的run()之前运行
		new Thread(td1).start();
		new Thread(td2).start();
	}
}

1、当DeadLock 类的对象flag=1时(td1),先锁定o1,睡眠500毫秒

2、而td1在睡眠的时候另一个flag==0的对象(td2)线程启动,先锁定o2,睡眠500毫秒

3、td1睡眠结束后需要锁定o2才能继续执行,而此时o2已被td2锁定;

4、td2睡眠结束后需要锁定o1才能继续执行,而此时o1已被td1锁定;

5、td1、td2相互等待,都需要得到对方锁定的资源才能继续执行,从而死锁。

还有人问你多线程的死锁?快把这篇「多线程之死锁详解」扔给他

 

动态锁顺序死锁:

// 资金转账到账号
 public static void transferMoney(Account fromaccount,
 Account toAccount,
 DollarAmount amount)
 throws InsufficientFundsException {
 // 锁定汇款者的账户
 synchronized (fromAccount) {
 // 锁定到账者的账户
 synchronized (toAccount) {
 // 判断账户的余额不能为负数
 if (fromAccount.getBalance().compareTo(amount) < 0) {
 throw new InsufficientFundsException();
 } else {
 // 汇款者的账户减钱
 fromAccount.debit(amount);
 // 到账者的账户增钱
 toAccount.credit(amount);
 }
 }
 }
 }

上面的代码看起来都是按照相同的顺序来获得锁的,按道理来说是没有问题,但是上述代码中上锁的顺序取决于传递给transferMoney()的参数顺序,而这些参数顺序又取决于外部的输入

  • 如果两个线程(A和B)同时调用transferMoney()
  • 其中一个线程(A),从X向Y转账:transferMoney(myAccount,yourAccount,10);
  • 另一个线程(B),从Y向X转账 :transferMoney(yourAccount,myAccount,20);
  • 此时 A线程 可能获得 myAccount 的锁并等待 yourAccount的锁,然而 B线程 此时已经持有 yourAccount 的锁,并且正在等待 myAccount 的锁,这种情况下就会发生死锁。

当一组JAVA线程发生死锁的时候,那么这些线程永远不能再使用了,根据线程完成工作的不同,可能会造成应用程序的完全停止,或者某个特定的子系统不能再使用了,或者是性能降低,这个时候恢复应用程序的唯一方式就是中止并重启它,死锁造成的影响很少会立即显现出来,如果一个类发生死锁,并不意味着每次都会发生死锁,而只是表示有可能,当死锁出现的时候,往往是在最糟糕的时候——在高负载的情况下。

四、死锁的避免与检测

4.1 预防死锁

  • 破坏互斥条件:使资源同时访问而非互斥使用,就没有进程会阻塞在资源上,从而不发生死锁
  • 破坏请求和保持条件:采用静态分配的方式,静态分配的方式是指进程必须在执行之前就申请需要的全部资源,且直至所要的资源全部得到满足后才开始执行,只要有一个资源得不到分配,也不给这个进程分配其他的资源。
  • 破坏不剥夺条件:即当某进程获得了部分资源,但得不到其它资源,则释放已占有的资源,但是只适用于内存和处理器资源。
  • 破坏循环等待条件:给系统的所有资源编号,规定进程请求所需资源的顺序必须按照资源的编号依次进行。

4.2 设置加锁顺序

如果两个线程(A和B),当A线程已经锁住了Z,而又去尝试锁住X,而X已经被线程B锁住,线程A和线程B分别持有对应的锁,而又去争夺其他一个锁(尝试锁住另一个线程已经锁住的锁)的时候,就会发生死锁,如下图:

还有人问你多线程的死锁?快把这篇「多线程之死锁详解」扔给他

 

两个线程试图以不同的顺序来获得相同的锁,如果按照相同的顺序来请求锁,那么就不会出现循环的加锁依赖性,因此也就不会产生死锁,每个需要锁Z和锁X的线程都以相同的顺序来获取Z和X,那么就不会发生死锁了,如下图所示:

还有人问你多线程的死锁?快把这篇「多线程之死锁详解」扔给他

 

  • 这样死锁就永远不会发生。 针对两个特定的锁,可以尝试按照锁对象的hashCode值大小的顺序,分别获得两个锁,这样锁总是会以特定的顺序获得锁,我们通过设置锁的顺序,来防止死锁的发生,在这里我们使用System.identityHashCode方法来定义锁的顺序,这个方法将返回由Obejct.hashCode 返回的值,这样就可以消除死锁发生的可能性。
public class DeadLockExample3 {
 // 加时赛锁,在极少数情况下,如果两个hash值相等,使用这个锁进行加锁
 private static final Object tieLock = new Object();
 public void transferMoney(final Account fromAcct,
 final Account toAcct,
 final DollarAmount amount)
 throws InsufficientFundsException {
 class Helper {
 public void transfer() throws InsufficientFundsException {
 if (fromAcct.getBalance().compareTo(amount) < 0)
 throw new InsufficientFundsException();
 else {
 fromAcct.debit(amount);
 toAcct.credit(amount);
 }
 }
 }
 // 得到两个锁的hash值
 int fromHash = System.identityHashCode(fromAcct);
 int toHash = System.identityHashCode(toAcct);
 // 根据hash值判断锁顺序,决定锁的顺序
 if (fromHash < toHash) {
 synchronized (fromAcct) {
 synchronized (toAcct) {
 new Helper().transfer();
 }
 }
 } else if (fromHash > toHash) {
 synchronized (toAcct) {
 synchronized (fromAcct) {
 new Helper().transfer();
 }
 }
 } else {// 如果两个对象的hash相等,通过tieLock来决定加锁的顺序,否则又会重新引入死锁——加时赛锁
 synchronized (tieLock) {
 synchronized (fromAcct) {
 synchronized (toAcct) {
 new Helper().transfer();
 }
 }
 }
 }
 }
}
  • 在极少数情况下,两个对象可能拥有两个相同的散列值,此时必须通过某种任意的方法来决定锁的顺序,否则可能又会重新引入死锁。
  • 为了避免这种情况,可以使用 “加时(Tie-Breaking))”锁,这获得这两个Account锁之前,从而消除了死锁发生的可能性

4.3 支持定时的锁(超时放弃)

有一项技术可以检测死锁和从死锁中恢复过来,就是使用Lock类中的定时public boolean tryLock(long time, TimeUnit unit) throws InterruptedException功能,来代替内置锁机制,当使用内置锁的时候,只要没有获得锁,就会永远等待下去,而tryLock可以指定一个超时时间(Timeout),在等待超过时间后tryLock会返回一个失败信息,如果超时时限比获取锁的时间要长很多,那么就可以在发生某个意外后重新获得控制权。如下图所示:

还有人问你多线程的死锁?快把这篇「多线程之死锁详解」扔给他

 

4.4 死锁避免

死锁防止方法能够防止发生死锁,但必然会降低系统并发性,导致低效的资源利用率,其中最具有代表性的避免死锁算法是银行家算法。

1、多个资源的银行家算法

还有人问你多线程的死锁?快把这篇「多线程之死锁详解」扔给他

 

上图中有五个进程,四个资源。左边的图表示已经分配的资源,右边的图表示还需要分配的资源。最右边的 E、P 以及 A 分别表示:总资源、已分配资源以及可用资源,注意这三个为向量,而不是具体数值,例如 A=(1020),表示 4 个资源分别还剩下 1/0/2/0。

检查一个状态是否安全的算法如下:

  • 查找右边的矩阵是否存在一行小于等于向量 A。如果不存在这样的行,那么系统将会发生死锁,状态是不安全的。
  • 假若找到这样一行,将该进程标记为终止,并将其已分配资源加到 A 中。
  • 重复以上两步,直到所有进程都标记为终止,则状态时安全的。
  • 如果一个状态不是安全的,需要拒绝进入这个状态。

4.5 死锁检测

  • 对资源的分配加以适当限制可防止或避免死锁发生,但不利于进程对系统资源的充分共享。
  • 为每个进程和每个资源指定一个唯一的号码
  • Jstack命令

jstack用于生成java虚拟机当前时刻的线程快照。线程快照是当前java虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的主要目的是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待,线程出现停顿的时候通过jstack来查看各个线程的调用堆栈,就可以知道没有响应的线程到底在后台做什么事情,或者等待什么资源。

  • JConsole工具

Jconsole是JDK自带的监控工具,在JDK/bin目录下可以找到。它用于连接正在运行的本地或者远程的JVM,对运行在Java应用程序的资源消耗和性能进行监控,并画出大量的图表,提供强大的可视化界面。而且本身占用的服务器内存很小,甚至可以说几乎不消耗。

4.5 死锁恢复

  • 资源剥夺:剥夺陷于死锁的进程所占用的资源,但并不撤销此进程,直至死锁解除
  • 进程回退:根据系统保存的检查点让所有的进程回退,直到足以解除死锁,这种措施要求系统建立保存检查点、回退及重启机制
  • 进程撤销:

1、撤销陷入死锁的所有进程,解除死锁,继续运行。2、逐个撤销陷入死锁的进程,回收其资源并重新分配,直至死锁解除。 可选择符合下面条件之一的先撤销: 1.CPU消耗时间最少者 2.产生的输出量最小者 3.预计剩余执行时间最长者 4.分得的资源数量最少者后优先级最低者

  • 系统重启:结束所有进程的执行并重新启动操作系统。这种方法很简单,但先前的工作全部作废。


Tags:多线程   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,如有任何标注错误或版权侵犯请与我们联系(Email:2595517585@qq.com),我们将及时更正、删除,谢谢。
▌相关推荐
一文看懂"async"和“await”关键词是如何简化了C#中多线程的开发过程当我们使用需要长时间运行的方法(即,用于读取大文件或从网络下载大量资源)时,在同步的应用程序中,应用程序本...【详细内容】
2021-12-01  Tags: 多线程  点击:(24)  评论:(0)  加入收藏
相关概念 并发和并行 并发:指一个时间段内,在一个CPU(CPU核心)能运行的程序的数量。 并行:指在同一时刻,在多个CPU上运行多个程序,跟CPU(CPU核心)数量有关。因为计算机CPU(CPU核心)在同...【详细内容】
2021-11-30  Tags: 多线程  点击:(18)  评论:(0)  加入收藏
一、现代计算机理论模型与工作方式现代计算机模型是基于-冯诺依曼计算机模型。计算机在运行时,先从内存中取出第一条指令,通过控制器的译码,按指令的要求,从存储器中取出数据进...【详细内容】
2021-07-19  Tags: 多线程  点击:(87)  评论:(0)  加入收藏
public class SReactorSThread { private Selector selector; private ServerSocketChannel serverSocketChannel; private int PORT = 6666; public SReactorS...【详细内容】
2021-05-25  Tags: 多线程  点击:(779)  评论:(0)  加入收藏
并发与并行分布式系统的一个重要特征就是计算能力是可以并发或者并行的。在分布式系统中,往往会将一个大任务进行分解,而后下发给不同的节点去计算,从而节省整个任务的计算时间...【详细内容】
2021-04-15  Tags: 多线程  点击:(235)  评论:(0)  加入收藏
年轻人,醒醒吧!此时不搏何时搏!本文主要讲一下常见的CAS理论。再者就是说一下锁的分类,什么乐观锁啊,悲观锁、重入锁等等。这篇文章要一网打尽,都介绍一下。把CAS按在地上摩擦中文...【详细内容】
2020-12-08  Tags: 多线程  点击:(100)  评论:(0)  加入收藏
你真的了解多线程吗?如果问你“为什么多线程可以提高程序运行效率?”,想必你会说“计算机并行执行任务啊,当然效率高!” 这显然不是一个内行该给出的答案。要知道,一个 CPU 在任何...【详细内容】
2020-11-10  Tags: 多线程  点击:(142)  评论:(0)  加入收藏
最近定位了在一个多线程服务器程序(OceanBase MergeServer)中,一个线程非法篡改另一个线程的内存而导致程序core掉的问题。定位这个问题历经曲折,尝试了各种内存调试的办法。往...【详细内容】
2020-10-27  Tags: 多线程  点击:(152)  评论:(0)  加入收藏
一、什么是分布式?分布式更多的是一个概念,是为了解决单个物理服务器容量和性能瓶颈问题而采用的优化手段,该领域需要解决的问题极多,在不同的技术层面上,又包括:分布式文件系统...【详细内容】
2020-10-14  Tags: 多线程  点击:(91)  评论:(0)  加入收藏
作为一个程序员,线程,进程,协程这些是我们必须要掌握的最基础的知识,这就好比数学家必须要学习的基本几何原理一样,没有它们,我们在编程的世界里寸步难行。并发和并行并发和并行是...【详细内容】
2020-10-09  Tags: 多线程  点击:(97)  评论:(0)  加入收藏
▌简易百科推荐
本文分为三个等级自顶向下地分析了glibc中内存分配与回收的过程。本文不过度关注细节,因此只是分别从arena层次、bin层次、chunk层次进行图解,而不涉及有关指针的具体操作。前...【详细内容】
2021-12-28  linux技术栈    Tags:glibc   点击:(3)  评论:(0)  加入收藏
摘 要 (OF作品展示)OF之前介绍了用python实现数据可视化、数据分析及一些小项目,但基本都是后端的知识。想要做一个好看的可视化大屏,我们还要学一些前端的知识(vue),网上有很多比...【详细内容】
2021-12-27  项目与数据管理    Tags:Vue   点击:(2)  评论:(0)  加入收藏
程序是如何被执行的&emsp;&emsp;程序是如何被执行的?许多开发者可能也没法回答这个问题,大多数人更注重的是如何编写程序,却不会太注意编写好的程序是如何被运行,这并不是一个好...【详细内容】
2021-12-23  IT学习日记    Tags:程序   点击:(9)  评论:(0)  加入收藏
阅读收获✔️1. 了解单点登录实现原理✔️2. 掌握快速使用xxl-sso接入单点登录功能一、早期的多系统登录解决方案 单系统登录解决方案的核心是cookie,cookie携带会话id在浏览器...【详细内容】
2021-12-23  程序yuan    Tags:单点登录(   点击:(8)  评论:(0)  加入收藏
下载Eclipse RCP IDE如果你电脑上还没有安装Eclipse,那么请到这里下载对应版本的软件进行安装。具体的安装步骤就不在这赘述了。创建第一个标准Eclipse RCP应用(总共分为六步)1...【详细内容】
2021-12-22  阿福ChrisYuan    Tags:RCP应用   点击:(7)  评论:(0)  加入收藏
今天想简单聊一聊 Token 的 Value Capture,就是币的价值问题。首先说明啊,这个话题包含的内容非常之光,Token 的经济学设计也可以包含诸多问题,所以几乎不可能把这个问题说的清...【详细内容】
2021-12-21  唐少华TSH    Tags:Token   点击:(10)  评论:(0)  加入收藏
实现效果:假如有10条数据,分组展示,默认在当前页面展示4个,点击换一批,从第5个开始继续展示,到最后一组,再重新返回到第一组 data() { return { qList: [], //处理后...【详细内容】
2021-12-17  Mason程    Tags:VUE   点击:(14)  评论:(0)  加入收藏
什么是性能调优?(what) 为什么需要性能调优?(why) 什么时候需要性能调优?(when) 什么地方需要性能调优?(where) 什么时候来进行性能调优?(who) 怎么样进行性能调优?(How) 硬件配...【详细内容】
2021-12-16  软件测试小p    Tags:性能调优   点击:(20)  评论:(0)  加入收藏
Tasker 是一款适用于 Android 设备的高级自动化应用,它可以通过脚本让重复性的操作自动运行,提高效率。 不知道从哪里听说的抖音 app 会导致 OLED 屏幕烧屏。于是就现学现卖,自...【详细内容】
2021-12-15  ITBang    Tags:抖音防烧屏   点击:(25)  评论:(0)  加入收藏
11 月 23 日,Rust Moderation Team(审核团队)在 GitHub 上发布了辞职公告,即刻生效。根据公告,审核团队集体辞职是为了抗议 Rust 核心团队(Core team)在执行社区行为准则和标准上...【详细内容】
2021-12-15  InfoQ    Tags:Rust   点击:(25)  评论:(0)  加入收藏
最新更新
栏目热门
栏目头条