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

“避开死锁泥潭:开发者必知的技巧与工具“

 “欲买桂花同载酒,终不似、少年游。”


在之前的有一期中,我谈到了关于“线程安全的问题”,那么在线程安全问题中,讲到了 synchronized 关键字,对于 synchronized 关键字的使用,是一定要特别注意的。因为如果使用不巧当的话,就会引来“死锁”的问题!前面简单的提到了“死锁”,那么这一期便来详细说说关于“死锁”,这是我觉得非常有意思的话题。

死锁:

死锁是一个计算机系统中的状态,尤其是在多线程或多进程的环境下,指的是两个或多个进程(或线程)因为相互等待对方持有的资源,而导致的永久阻塞状态。

死锁出现的场景:

1.一个线程一把锁,这个线程针对这把锁,连续加锁两次。

那么这是一种怎样的场景呢?我们一起来看一看代码案例:

public class Demo1 {private static Object locker = new Object();public static void main(String[] args) {Thread t = new Thread(()->{synchronized (locker) {synchronized (locker) {System.out.println("hello thread");}}});t.start();}
}

我们可以看见代码中,synchronized代码段中,嵌套了另外一个synchronized,都是对于同一个对象进行加锁。那么在第一个synchronized中,是能够加锁成功的。那么对于第二个synchronized呢?它能够加锁成功吗?

由于第一个synchronized先拿到locker这个锁对象,第二个synchronized在尝试对locker对象进行加锁时,锁对象已经被占用就会进入堵塞状态。第二个synchronized在什么情况下会拿到 locker 对象呢,就是等第一个 synchronized 释放锁后,才会拿到。由于第二个synchronized阻塞着,所以第一个synchronized并不会有释放锁的机会,那么此时的情况就是一种“死锁”。(就相当于一个门被加锁两次,有一把锁还不是自己加的,所以怎么样也开不了)

那么真如我们上述所说的如此吗?下面运行代码来看看,是否会执行“hello thread”!

显示台上打印出了“hello thread”,那么我上面说的一堆,那简直是在虾扯蛋呀!

其实是因为Java中,synchronized 针对这种情况做了特殊处理,synchronized 是“可重入锁”。针对上述,一个线程连续加锁两次的情况做了特殊处理。那么它是怎么处理的呢?下面我们一起来看一看咯:

加锁的时候,是需要判定,当前这个锁是否是 “被占用” 的状态,可重入锁,就是在锁中,额外的记录一下,当前是哪个线程,对这个锁加锁了。

这就相当于,我给我的女朋友进行加锁了(确认男女朋友关系)。女朋友就会记录一下,她的持有人是我。此时如果隔壁班的老王同学,想对我的女朋友进行加锁,老王对我的女朋友说:“我喜欢你。”我的女朋友说;“滚,人家已经是有夫之妻”。如果是我对我女朋友说:“我喜欢你”!我女朋友就会说:“我也喜欢你”!

对于可重入锁来说,如果发现加锁的线程就是当前锁的持有线程,并不会真正进行任何操作,也不会进行任何的“阻塞操作”,而是直接发行,往下执行代码。

那么上述如果里面嵌套着很多,那么怎样知道那一次 } ,是进行真正的释放锁的操作呢?

我们可以引入一个计数器 count,

  1. 初始情况下时,count = 0;
  2. 每次执行到 { count+1;
  3. 每次执行到 } count-1;
  4. 如果在某一次 -1 之后,count = 0,此时就可以进行真正的释放锁操作了

可重入锁引入之后,为了避免出现上述一个线程连续加锁多次,“死锁”的情况,synchronized就是可重入锁。可重入锁内部记录了当前是哪个线程持有的锁,后续再进行加锁的时候,都会先进行判定,还会通过一个计数器来维护当前已经加锁了几次,以至于后面可以准确的时机释放锁。


 2.两个线程,两把锁

线程1          线程2       锁A       锁B

1.线程1先针对 A 进行加锁,线程2针对 B 进行加锁

2.在线程1不释放锁 A 的情况下,再针对 B 进行加锁,同时线程2不释放的情况下,针对 A 进行加锁。

public class Demo2 {private static Object locker1 = new Object();private static Object locker2 = new Object();public static void main(String[] args) {Thread t1 = new Thread(() -> {synchronized (locker1) {System.out.println("线程t1 对locker1 加锁成功");//这里使用sleep,是确保t1对locker1加锁成功,t2对locker2加锁成功。如果是t1直接加锁了locker1 和 locker2,就不会出现“死锁了”try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}synchronized (locker2) {System.out.println("线程t1 对locker2 加锁成功");}}});Thread t2 = new Thread(() -> {synchronized (locker2) {System.out.println("线程t2 对locker2 加锁成功");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}synchronized (locker1) {System.out.println("线程t2 对locker1 加锁成功");}}});t1.start();t2.start();}}

此时,我们来运行代码看看效果:

我们可以看见代码还在运行着,显示台也只打印了两段话,后面的打印代码无法执行到,此时这种情况也是一种死锁!

那么我们怎样来理解这个死锁的情况呢,我来举一个生活中的例子:假设有一天,我和我的女朋友出去吃粉,吃粉的时候我们都会放酱油和醋,当两碗粉端上来之后,我拿起了醋,女朋友拿起了酱油。她说:“把醋给我,我放完之后,两个都给你”,我说:“凭什么,把酱油给我,等我放完都给你”。我们僵持不下,谁都不给谁,就这样僵持着(死锁)。

3.N个线程 M个锁

一个经典模型:哲学家就餐问题。

有五个哲学家围坐在圆桌旁,他们的生活主要是思考和进餐。每个哲学家有两种状态:思考人生和进餐。每当他们想要吃饭时,需要拿起左边和右边的筷子,而叉子是共享的。桌子上只放置了五根叉子。

 假设,哲学家1和哲学家都想吃面条,哲学家1拿起了左右手的筷子吃了起来,此时哲学家2就只能拿到左手边的筷子,不能完成吃面条的操作。就只能等哲学家1吃完后,哲学家2才能吃,此时这种情况下是构不成死锁的。如果是在极端情况下呢,所有的哲学家都想吃面条,此时他们都拿起了左手边的筷子,此时都拿不到右手边的筷子,完成不了进餐的操作,由于哲学家非常的固执,他吃不到面条时,也不会放下手中的筷子,此时所有的哲学家都在等待叉子,但没有人能进餐,也是一直这样僵持着,此时也构成了 死锁!

那么我们怎样来解决死锁的问题呢?我们先来看一看死锁的四个必要条件:

  1. 锁是互斥的 【锁的基本特性】
  2. 锁是不可被抢占的,线程1拿到了锁A,如果线程1不主动释放锁A的话,线程2是拿不到锁A的【锁的基本特性】
  3. 请求和保持,线程1,拿到锁A之后,不释放锁A的情况下,去拿锁B【代码结构】
  4. 循环等待/环路等待/循环依赖 多个线程获取锁的过程,存在 循环等待 【代码结构】

那么实例2中,如果不按照请求保持的方式,此时就不会出现死锁的情况了:

public class Demo2 {private static Object locker1 = new Object();private static Object locker2 = new Object();public static void main(String[] args) {Thread t1 = new Thread(() -> {synchronized (locker1) {System.out.println("线程t1 对locker1 加锁成功");//这里使用sleep,是确保t1对locker1加锁成功,t2对locker2加锁成功。如果是t1直接加锁了locker1 和 locker2,就不会出现“死锁了”try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}synchronized (locker2) {System.out.println("线程t1 对locker2 加锁成功");}});Thread t2 = new Thread(() -> {synchronized (locker2) {System.out.println("线程t2 对locker2 加锁成功");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}synchronized (locker1) {System.out.println("线程t2 对locker1 加锁成功");}});t1.start();t2.start();}}

 此时我们的代码就是先释放锁A,再去拿锁B,就不会有问题.

假设代码按照请求保持的方式,获取到N个锁,如何避免出现循环等待呢?

一个简单有效的办法:给锁编号,1,2,3,4......N,约定所有的线程在加锁的时候,都必须按照一定的顺序来加锁(比如,先针对编号小的锁,加锁,后针对编号大的加锁)

那么对于哲学家就餐问题中,五支筷子就相当于五个锁,我们给锁进行编号,且规定着每个滑稽加锁的时候,一定是先拿起编号小的筷子,后拿起编号大的筷子。

同一时刻,所有线程拿起第一支筷子:假设此时,我们的哲学家2拿到了筷子1,哲学家3拿到筷子2,哲学家4拿到筷子3,哲学家5拿到筷子4,由于规定哲学家要先去拿筷子1再去拿筷子5,但是筷子1被别人拿走了,所以哲学家1就只能阻塞着。由于哲学家1阻塞,就拿不到筷子5,此时哲学家5就能拿到了。

那么哲学家5就会拿到右手边筷子5,此时哲学家5就会完成进餐的操作,哲学家5心满意足之后,就会放下筷子,此时哲学家4就可以拿起右手边的筷子了,以此类推,直到全部完成。

那么我们规定好顺序之后,的代码案例2,还可以这样改:

public class Demo3 {private static Object locker1 = new Object();private static Object locker2 = new Object();public static void main(String[] args) {Thread t1 = new Thread(() -> {synchronized (locker1) {System.out.println("线程t1 对locker1 加锁成功");//这里使用sleep,是确保t1对locker1加锁成功,t2对locker2加锁成功。如果是t1直接加锁了locker1 和 locker2,就不会出现“死锁了”try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}synchronized (locker2) {System.out.println("线程t1 对locker2 加锁成功");}}});Thread t2 = new Thread(() -> {synchronized (locker1) {System.out.println("线程t2 对locker1 加锁成功");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}synchronized (locker2) {System.out.println("线程t2 对locker2 加锁成功");}}});t1.start();t2.start();}
}

那么对于死锁的的四个基本必要条件中,前两个是锁的基本特性,我们无法改变。如果要避免死锁的情况,此时我们两个简单有效的办法就是:

  1. 避免锁嵌套
  2. 约定加锁顺序

文章中可能会存在一些错误和小问题,欢迎大家的指正和观点!

“有些故事未完待续,欢迎回到这里,与我一起继续书写我们的篇章。”

下一期再相遇!

相关文章:

  • 提升SAP归档效率的5个实用技巧
  • 智慧城市交通管理中的云端多车调度与控制
  • uniapp数据缓存
  • C#源码安装ZedGraph组件,并且立即演示使用
  • GIS在构建虚拟世界中的新机遇
  • 滚雪球学MySQL[1.1讲]:MySQL简介与环境配置
  • el-upload自定上传列表删除,上传列表已删除,提交数据仍存在问题
  • 什么情况?上交所服务器被你们给买崩了?
  • 将Mixamo的模型和动画导入UE5
  • Android OpenGLES2.0开发(三):绘制一个三角形
  • 全方位助力“生活家”丨约克VRF中央空调UDIII舒享系列引领美好生活新潮流
  • Leetcode面试经典150题-39.组合总数进阶:40.组合总和II
  • 【OpenCV】 Python 图像处理 入门
  • vscode 顶部 Command Center,minimap
  • php中根据指定日期获取所在天,周,月,年的开始日期与结束日期
  • 【EOS】Cleos基础
  • 【mysql】环境安装、服务启动、密码设置
  • angular组件开发
  • axios 和 cookie 的那些事
  • CSS进阶篇--用CSS开启硬件加速来提高网站性能
  •  D - 粉碎叛乱F - 其他起义
  • ES2017异步函数现已正式可用
  • Flannel解读
  • JavaScript的使用你知道几种?(上)
  • leetcode388. Longest Absolute File Path
  • OpenStack安装流程(juno版)- 添加网络服务(neutron)- controller节点
  • springMvc学习笔记(2)
  • Swift 中的尾递归和蹦床
  • Vue UI框架库开发介绍
  • vue2.0开发聊天程序(四) 完整体验一次Vue开发(下)
  • 读懂package.json -- 依赖管理
  • 发布国内首个无服务器容器服务,运维效率从未如此高效
  • 利用DataURL技术在网页上显示图片
  • 配置 PM2 实现代码自动发布
  • 如何编写一个可升级的智能合约
  • 使用 @font-face
  • 3月7日云栖精选夜读 | RSA 2019安全大会:企业资产管理成行业新风向标,云上安全占绝对优势 ...
  • 带你开发类似Pokemon Go的AR游戏
  • 扩展资源服务器解决oauth2 性能瓶颈
  • 如何在招聘中考核.NET架构师
  • ​埃文科技受邀出席2024 “数据要素×”生态大会​
  • # wps必须要登录激活才能使用吗?
  • #如何使用 Qt 5.6 在 Android 上启用 NFC
  • #知识分享#笔记#学习方法
  • (2022 CVPR) Unbiased Teacher v2
  • (C#)一个最简单的链表类
  • (pt可视化)利用torch的make_grid进行张量可视化
  • (超简单)构建高可用网络应用:使用Nginx进行负载均衡与健康检查
  • (纯JS)图片裁剪
  • (附源码)ssm高校志愿者服务系统 毕业设计 011648
  • (附源码)ssm捐赠救助系统 毕业设计 060945
  • (规划)24届春招和25届暑假实习路线准备规划
  • (六)Flink 窗口计算
  • (强烈推荐)移动端音视频从零到上手(下)
  • (学习日记)2024.04.04:UCOSIII第三十二节:计数信号量实验