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

embedded/2024/9/30 0:58:59/

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


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

死锁

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

死锁出现的场景:

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

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

java">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 进行加锁。

java">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中,如果不按照请求保持的方式,此时就不会出现死锁的情况了:

java">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,还可以这样改:

java">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. 约定加锁顺序

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

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

下一期再相遇!


http://www.ppmy.cn/embedded/119657.html

相关文章

开源 AI 智能名片与 S2B2C 商城小程序:嫁接权威实现信任与增长

摘要:本文探讨了嫁接权威在产品营销中的重要性,并结合开源 AI 智能名片与 S2B2C 商城小程序,阐述了如何通过与权威关联来建立客户信任,提升产品竞争力。强调了在当今商业环境中,巧妙运用嫁接权威的方法,能够…

算法题题解:分隔链表

Problem: 86. 分隔链表 题目描述: 给定一个链表和一个值 x,要求将链表重新排列,所有小于 x 的节点放在前面,所有大于或等于 x 的节点放在后面。要求保留节点的相对顺序。 解题思路: 因为是链表而不是数组&#xff0c…

python股票因子,交易所服务器宕机,量化交易程序怎么应对

炒股自动化:申请官方API接口,散户也可以 python炒股自动化(0),申请券商API接口 python炒股自动化(1),量化交易接口区别 Python炒股自动化(2):获取…

828华为云征文 | 云服务器Flexus X实例:开源数据库 mysql 部署,轻量级数据库

目录 一、MySQL 介绍 1.1 MySQL 优势 二、MySQL 部署 2.1 安装 Docker 2.2 拉取 MySQL 镜像 2.3 添加规则 三、MySQL 运行 3.1 运行 MySQL 镜像 3.2 安装 mysql 客户端 3.2 本地连接 mysql 数据库 四、总结 本篇文章介绍在 云服务器Flexus X实例 上安装开源数据库 M…

PostgreSQL的表碎片

PostgreSQL的表碎片 在 PostgreSQL 中,表碎片化可能会影响数据库性能和存储效率。碎片化通常是由于频繁的插入、更新和删除操作引起的。以下是关于 PostgreSQL 表碎片化的详细信息,包括如何识别和处理表碎片化。 什么是表碎片化? 表碎片化…

消息队列常见面试题总结

文章目录 1 消息队列有什么用 ?2 使用消息队列会带来哪些问题3 如何保证 RabbitMQ 消息的顺序性?🔥4 RabbitMQ-如何保证消息不丢失🔥5 RabbitMQ 消息的重复消费问题如何解决的🔥6 RabbitMQ 延迟队列⭐7 RabbitMQ 消息怎…

diffusion model(3) 扩散模型去噪推理原理 solver

在之前的篇章中,以huggingface为例,分析了模型在训练阶段,是如何加噪声,以及用unet预测噪声的。接下来以开源代码diffusers为例,分析扩散模型在去噪推理时的原理。 在阅读这篇文章前,读者需要先了解DDPM、L…

SDL录制音频并播放

摘要:在ubuntu 20.04中使用QAudioInput、PortAduio、ffmpeg打开音频设备录制音频都显示失败,最后没办法选择了SDL2.0,SDL2.0录制音频十分方便,使用也非常简单。 疑问:不知道SDL是如何区分打开的设备是录制还是播放。&a…