当前位置: 首页 > news >正文

JavaEE之多线程编程:5. 死锁(详解!!!)

文章目录

    • 一、死锁是什么
    • 二、关于死锁的三种形式
    • 三、如何避免死锁

一、死锁是什么

死锁是这样的一种情形:多个同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。
在这里插入图片描述

【举个例子理解死锁】
张三李四两人去吃饺子,吃饺子需要酱油和醋。
张三抄起了酱油瓶, 李四抄起了醋瓶。
张三:你先把醋瓶给我,我用完了就把酱油瓶给你。
李四:你先把酱油瓶给我,我用完了就把醋瓶给你。
如果这俩人彼此之间互不相让,就构成了死锁。
酱油和醋相当于是两把锁,这两个人就是两个线程。

二、关于死锁的三种形式

  1. 一个线程,针对同一把锁连续加锁两次,如果是不可重入锁,就死锁了。
    关于可重入锁:指的是一个线程连续针对一把锁,加锁了两次且不会出现死锁,满足这个要求就是可重入锁。
  2. 两个线程,两把锁(此时无论是不是不可重入锁,都会死锁)。
    如:有两个线程 t1、t2,有两把锁 A、B
    ① t1 获取锁 A,t2 获取锁 B;
    ② t1 尝试获取锁 B,t2 尝试获取锁 A。
    此时就会阻塞,产生死锁。
package Thread;/*** @Author : tipper* @Description : 死锁的情况*/
public class Demo4 {//锁1private static Object locker1 = new Object();//锁2private static Object locker2 = new Object();public static void main(String[] args) {//线程1Thread t1 = new Thread(()->{//加锁synchronized (locker1) {//此处的sleep很重要,要确保t1和t2都分别拿到一把锁之后。再进行后续动作。try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}synchronized (locker2) {System.out.println("t1 加锁成功!");}}});//线程2Thread t2 = new Thread(()->{//加锁synchronized (locker2) {try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}synchronized (locker1) {System.out.println("t2 加锁成功!");}}});}
t1.start();
t2.start();
}//输出为空且一直在执行

上述代码中,很明显什么都没打印,两个线程都没有获取成功第二把锁。
在此时,死锁代码中,两个 synchronized 是嵌套关系,不是并列关系,嵌套关系说明是在占用一把锁的前提下,获取另一把锁;而并列关系则是先释放前面的锁,再获取下一把锁,这样就不会死锁了。

  1. N个线程,M把锁。(相当于第2种情况的扩充)
    此时是更容易出现死锁的情况了。
    一个经典的描述N个线程M把锁死锁的模型,哲学家就餐问题。

【哲学家就餐问题】
在这里插入图片描述

① 每个哲学家都会做两件事:思考人生,放下筷子 和 拿起左右两侧的两根筷子开始吃面条(先拿起左边,再拿起右边);
② 哲学家什么时候吃面条和思考人生都是随机的;
③ 哲学家吃面条吃多久吃完也是随机的;
④ 哲学家吃面条的过程中,会有左右相邻的哲学家如果也想吃面条,就要阻塞等待。

基于上述规则,通常情况下,整个系统可以良好运转,但是极端情况下会出现问题!
比如,同一时刻,五个哲学家都想吃面条,同时拿起左手的筷子,然后再尝试拿右手的筷子,就会发现右手的筷子都被占用了,哲学家们互不相让,这个时候就形成了死锁
在这里插入图片描述
死锁是一种严重的BUG!会导致程序的线程“卡死”,无法正常工作!

三、如何避免死锁

死锁产生的四个必要条件:

  • 互斥使用(锁的基本特性):当一个线程持有一把锁之后,另一个线程想要取得锁,就要阻塞等待;
  • 不可抢占(锁的基本特性):资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。(当锁已经被 线程1 拿到之后,线程2 只能等 线程1 主动释放,不能强行抢过来)
  • 请求和保持(代码结构):当资源请求者在请求其他的资源的同时保持对原有资源的占有。(一个线程尝试获取多把锁,先拿到 锁1 之后,再尝试获取 锁2,获取的时候,锁1 不会释放)。
  • 循环等待:即存在一个等待队列,P1占有P2的资源,P2占有P3的资源,P3占有P1的资源,等待的依赖关系,这样就形成了一个等待环路。

当上述四个条件都成立的时候,便形成死锁。当然,死锁的情况下如果打破上述任何一个条件,便可让死锁消失。

其中最容易破坏的就是“循环等待”。“互斥使用”、“不可抢占”是锁本身的特性,破坏不了,对于“请求保持”来说,调整代码结构,避免编写“锁嵌套”逻辑。这个方案不一定好用,有的需求可能就是需要进行这种,获取多个锁再操作。

【破坏循环等待】
最常用的一种死锁组织技术就是锁排序。约定加锁的顺序,就可以避免循环等待。
假设有 N 个线程尝试获取 M 把锁,就可以针对 M 把锁进行编号(1,2,3…M)。
N 个线程尝试获取锁的时候,都按照固定的按编号由小到大顺序来获取锁。这样就可以避免环路等待。

比如:哲学家就餐问题,约定每个哲学家都是先拿起编号小的筷子,后拿起编号大的筷子,此时循环等待就破除了,上述系统就不会再出现死锁了。
在这里插入图片描述

【可能产生环路等待的代码】
两个线程对于加锁的顺序没有约定,就容易产生环路等待。

package Thread;
public class Demo4 {//锁1private static Object locker1 = new Object();//锁2private static Object locker2 = new Object();public static void main(String[] args) {//线程1Thread t1 = new Thread(()->{//加锁synchronized (locker1) {//此处的sleep很重要,要确保t1和t2都分别拿到一把锁之后。再进行后续动作。try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}});synchronized (locker2) {System.out.println("t1 加锁成功!");}//线程2Thread t2 = new Thread(()->{//加锁synchronized (locker2) {try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}});synchronized (locker1) {System.out.println("t2 加锁成功!");}}
}//运行结果:
此时一直运行

【不会产生环路等待的代码】
约定号先获取 locker1,再获取 locker2,就不会环路等待。

package Thread;/*** @Author : tipper* @Description : 死锁的情况*/
public class Demo4 {//锁1private static Object locker1 = new Object();//锁2private static Object locker2 = new Object();public static void main(String[] args) {//线程1Thread t1 = new Thread(()->{//加锁synchronized (locker1) {//此处的sleep很重要,要确保t1和t2都分别拿到一把锁之后。再进行后续动作。try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}synchronized (locker2) {System.out.println("t1 加锁成功!");}}});//线程2Thread t2 = new Thread(()->{//加锁synchronized (locker1) {try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}synchronized (locker2) {System.out.println("t2 加锁成功!");}}});t1.start();t2.start();}}
//运行结果:
t1 加锁成功!
t2 加锁成功!

相关文章:

  • 《设计模式的艺术》笔记 - 观察者模式
  • Rsync服务
  • R语言【taxa】——n_subtaxa(),n_supertaxa():每个类群的子类群数量和父类群数量
  • 使用__missing__方法实现映射表多格式主键
  • Windows AD 组策略 通过脚本修改管理员密码:以安全方式
  • nc转tif
  • 全栈工程师
  • 【C++入门到精通】智能指针 shared_ptr循环引用 | weak_ptr 简介及C++模拟实现 [ C++入门 ]
  • 【笔记】Helm-4 最佳实践-2 values
  • 01.领域驱动设计:微服务设计为什么要选择DDD学习总结
  • 2024年【G2电站锅炉司炉】新版试题及G2电站锅炉司炉作业考试题库
  • Layui技术积累
  • Qt Quick程序的发布|Qt5中QML和Qt Quick 的更改
  • 【GitHub项目推荐--不错的 Go 学习项目】【转载】
  • ZYNQ程序固化
  • (ckeditor+ckfinder用法)Jquery,js获取ckeditor值
  • Angular2开发踩坑系列-生产环境编译
  • centos安装java运行环境jdk+tomcat
  • CSS盒模型深入
  • FineReport中如何实现自动滚屏效果
  • gulp 教程
  • JavaScript HTML DOM
  • javascript从右向左截取指定位数字符的3种方法
  • JAVA之继承和多态
  • QQ浏览器x5内核的兼容性问题
  • Travix是如何部署应用程序到Kubernetes上的
  • 从setTimeout-setInterval看JS线程
  • 从零搭建Koa2 Server
  • 对话:中国为什么有前途/ 写给中国的经济学
  • 机器人定位导航技术 激光SLAM与视觉SLAM谁更胜一筹?
  • 记一次和乔布斯合作最难忘的经历
  • 聊聊redis的数据结构的应用
  • 前端工程化(Gulp、Webpack)-webpack
  • 日剧·日综资源集合(建议收藏)
  • 如何优雅地使用 Sublime Text
  • 再次简单明了总结flex布局,一看就懂...
  • 东超科技获得千万级Pre-A轮融资,投资方为中科创星 ...
  • 基于django的视频点播网站开发-step3-注册登录功能 ...
  • ​html.parser --- 简单的 HTML 和 XHTML 解析器​
  • $().each和$.each的区别
  • $GOPATH/go.mod exists but should not goland
  • (2021|NIPS,扩散,无条件分数估计,条件分数估计)无分类器引导扩散
  • (33)STM32——485实验笔记
  • (delphi11最新学习资料) Object Pascal 学习笔记---第7章第3节(封装和窗体)
  • (Java)【深基9.例1】选举学生会
  • (ZT)出版业改革:该死的死,该生的生
  • (分布式缓存)Redis分片集群
  • (附源码)spring boot火车票售卖系统 毕业设计 211004
  • (附源码)spring boot网络空间安全实验教学示范中心网站 毕业设计 111454
  • (每日持续更新)jdk api之StringBufferInputStream基础、应用、实战
  • (学习日记)2024.01.09
  • (自用)learnOpenGL学习总结-高级OpenGL-抗锯齿
  • .Net 8.0 新的变化
  • .NET C#版本和.NET版本以及VS版本的对应关系
  • .NET 中选择合适的文件打开模式(CreateNew, Create, Open, OpenOrCreate, Truncate, Append)