关于我对线程安全问题中死锁的理解
创始人
2024-03-12 16:50:15
0

文章目录

  • 1.什么是死锁
  • 2.三个典型情况
  • 3.可重入与不可重入
  • 4.死锁的四个必要条件
  • 5.如何破除死锁

1.什么是死锁


比如张三谈了一个女朋友,张三就对这个女朋友加锁了。
此时李四也看上了这个女生,但是他只能等待张三分手(解锁)后,才能和这个女生谈恋爱。

李四为了等待这个女生,错过了好多喜欢他的人,这里就相当于线程无法执行的后续工作,
此时就相当于是死锁了。

一旦程序出现死锁,就会导致线程无法执行后序工作了,此时程序必然会有严重的 bug 。
发生死锁的概率又是随机的,因此死锁是非常隐蔽,不容易被发现的。

2.三个典型情况

情况1:

一个线程如果有一把锁,连续加锁两次。如果这个锁是不可重入锁,就会死锁。

java 中的 synchronizedReentrantLock 都是可重入锁,
因此这一种情况演示不了。


情况2:

两个线程两把锁,t1 和 t2 各自先针对 锁1 和 锁2 加锁,之后再尝试获取对方的锁。

比如说,张三的车钥匙锁在屋里了,而屋子的钥匙锁在车了。
这个时候屋子和车都进不去了,就会产生问题。

下面来举例说明。

package thread;public class ThreadDemo16 {public static void main(String[] args) {Object locker1 = new Object();Object locker2 = new Object();Thread t1 = new Thread(() -> {synchronized (locker1) {try {Thread.sleep(500);} catch (InterruptedException e) {e.printStackTrace();}synchronized (locker2) {System.out.println("线程t1拿到两个锁");}}});Thread t2 = new Thread(() -> {synchronized (locker2) {try {Thread.sleep(500);} catch (InterruptedException e) {e.printStackTrace();}synchronized (locker1) {System.out.println("线程t2拿到两个锁");}}});t1.start();t2.start();}
}


这里并没有输出结果,说明线程并没有拿到两把锁。

这个时候可以使用 jconsole 来查看当前的进程的情况。


按照这样的路径查找 jconsole ,然后双击。



看到这样的窗口,双击选中的进程。






红色框框的表示 获取锁获取不到的阻塞状态
绿色框框的表示 发生错误的代码行数






情况3: 多个线程,多把锁。(向较与情况2的一半情况

例子:哲学家就餐问题



每个哲学家有两种状态:

  1. 思考人生(相当于线程的阻塞状态
  2. 拿起筷子吃面条(想当于线程获取到锁然后执行一些操作)、

由于操作系统的随机调度,这五个哲学家,随时都可能想吃面条,也随时可能要思考人生。

如果想吃面条就需要拿起左手和右手的筷子。


假如同一时刻,所有的哲学家都拿起左手的筷子吃面条。
此时如果要成功吃到面条,就要等到右边的哲学家放下手中的筷子,自己才可以吃到面条。
此时就会死锁!!!

只有一只筷子没有办法吃面条,必须要等到右边的老铁放下筷子才可以吃。
如果右边一直不放,左边的老铁就一直吃不到。

3.可重入与不可重入

一个线程针对同一个对象,如果不会发生问题就叫可重入的,否则就叫不可重入的。

class Counter {public int count = 0;synchronized public void add() {synchronized(this) {count++;}}
}

锁对象是 this ,只要有线程调用 add 方法,进入 add 方法的时候,
在可以加锁成功的前提下,就会先加锁。紧接着又遇到了代码块,此时会再次尝试加锁。

站在 锁对象的视角(this),他认为自己已经被其他的线程给占用了,
那么这里的第二次加锁是不需要阻塞等待的。

如果允许上述操作,这个锁就是可重入的;不允许就是不可重入的。
如果是不可重入的,就会发生 死锁。

上面演示的就是不可重入的死锁。


下面演示的是可重入的思索。

java 为了避免不小心出现闭锁现象,就把 synchronized 给设置成可重入的了。
因此 java 中才会无法演示上面的情况1.

4.死锁的四个必要条件

1、互斥使用 — 线程1拿到了锁,线程1就需要等待着。

2、不可抢占 — 线程1拿到锁之后,如果线程1不释放锁,线程2就不能强行获取。

3、请求和等待 — 线程1获取到锁A之后,再去获取到锁B,
此时锁A还会继续被线程1获取。(不会因为获取锁B后就把锁A给释放了)

4、循环等待 — 线程1尝试获取到锁A锁B,线程2尝试获取到锁B锁A
线程1在获取B的时候等待线程2释放B,同时线程2在获取A的时候等待线程1释放A

5.如何破除死锁

打破循环等待这个必要条件。

解决办法:

给每个筷子编号,指定固定的顺序(从小到大)拿筷子。


上图是规定从小到大的拿。


到最后一个老铁拿的时候,会拿一号筷子。
但是这个一号筷子被其他的老铁拿了,此时这个老铁就发生阻塞等待了。
此时拿四号筷子的老铁会把五号筷子也拿了,之后开始吃面条。


这个老铁吃面条的时候,拿三号筷子的老铁就会看着他吃。
等待这个老铁吃完,放下两支筷子号筷子,三号筷子的老铁就可以拿起四号筷子来吃了。


按照这样的方式,所有的老铁都可以吃面条。


下面由代码来演示:

package thread;public class ThreadDemo16 {public static void main(String[] args) {Object locker1 = new Object();Object locker2 = new Object();//给两个锁编号:1 、2,规定locker1是1号,locker2是2号,按照从小到大的顺序拿Thread t1 = new Thread(() -> {//先拿序号小的synchronized (locker1) {try {Thread.sleep(500);} catch (InterruptedException e) {e.printStackTrace();}//后拿序号大的synchronized (locker2) {System.out.println("线程t1拿到两个锁");}}});Thread t2 = new Thread(() -> {synchronized (locker1) {try {Thread.sleep(500);} catch (InterruptedException e) {e.printStackTrace();}synchronized (locker2) {System.out.println("线程t2拿到两个锁");}}});t1.start();t2.start();}
}



这种方法是解决死锁,最简单最可靠的方法。

相关内容

热门资讯

不能访问光猫的的管理页面 光猫是现代家庭宽带网络的重要组成部分,它可以提供高速稳定的网络连接。但是,有时候我们会遇到不能访问光...
银河麒麟V10SP1高级服务器... 银河麒麟高级服务器操作系统简介: 银河麒麟高级服务器操作系统V10是针对企业级关键业务...
【NI Multisim 14...   目录 序言 一、工具栏 🍊1.“标准”工具栏 🍊 2.视图工具...
Android|无法访问或保存... 这个问题可能是由于权限设置不正确导致的。您需要在应用程序清单文件中添加以下代码来请求适当的权限:此外...
北信源内网安全管理卸载 北信源内网安全管理是一款网络安全管理软件,主要用于保护内网安全。在日常使用过程中,卸载该软件是一种常...
AWSECS:访问外部网络时出... 如果您在AWS ECS中部署了应用程序,并且该应用程序需要访问外部网络,但是无法正常访问,可能是因为...
​ToDesk 远程工具安装及... 目录 前言 ToDesk 优势 ToDesk 下载安装 ToDesk 功能展示 文件传输 设备链接 ...
AWSElasticBeans... 在Dockerfile中手动配置nginx反向代理。例如,在Dockerfile中添加以下代码:FR...
AsusVivobook无法开... 首先,我们可以尝试重置BIOS(Basic Input/Output System)来解决这个问题。...
ASM贪吃蛇游戏-解决错误的问... 要解决ASM贪吃蛇游戏中的错误问题,你可以按照以下步骤进行:首先,确定错误的具体表现和问题所在。在贪...