【Java多线程】分析线程加锁导致的死锁问题以及解决方案

目录

1、线程加锁

2、死锁问题的三种经典场景

2.1、一个线程一把锁

2.2、两个线程两把锁

2.3、N个线程M把锁(哲学家就餐问题)

 3、解决死锁问题


1、线程加锁

其中 locker 可以是任意对象,进入 synchronized 修饰的代码块, 相当于加锁,退出 synchronized 修饰的代码块, 相当解锁。

如果一个线程,针对一个对象加上锁之后,其他线程也尝试对这个对象加锁,就会导致锁竞争进而引起阻塞(BLOCKED),这个阻塞会一直持续到上一个线程释放锁为止。

如果是两个线程分别针对不同的对象进行加锁,此时不会由锁竞争,也就不会阻塞。

出现锁竞争进而引起阻塞状态,这个阻塞会一直持续到下一个线程释放锁为止。

但是,设想一个场景,共有AB两个线程,此时A线程因为锁竞争进入阻塞状态,而如果此时B线程恰巧也正在阻塞状态,由于AB线程都进入了阻塞状态,此时进程无法运行,出现死锁问题。下面针对死锁问题的出现以及解决方法展开讨论。

2、死锁问题的三种经典场景

2.1、一个线程一把锁

public static void main(String[] args) {
    Object locker = new Object();
    Thread t = new Thread(() -> {
        synchronized (locker) {   //两次加锁,加的是同一把锁
            synchronized (locker) {   //两次加锁,加的是同一把锁
                System.out.println("hello synchronized");
            }
        }
    });
    t.start();
}

需要注意的是,这里最直观的感觉是进行了两次加锁,会发生锁冲突。第一次针对locker加锁之后,在还没释放锁的时候又尝试对locker加锁,理论会出现锁冲突。

至于事实上是否会出现所冲突进而出现死锁,需要分情况讨论:

1、如果是不可重入锁,则就会出现锁竞争引起死锁。

2、如果是可重入锁,则不会出现锁竞争引起死锁,Java中的锁就是可重入锁,因此可以正常打印。

可以把这种情况理解成:【屋钥匙锁在了屋里】

2.2、两个线程两把锁

package thread;

public class ThreadDemo22 {
    public static void main(String[] args) {
        Object A = new Object();
        Object B = new Object();
        Thread t1 = new Thread(() -> {
            synchronized (A) {
                // sleep一下, 给 t2 时间, 让 t2 也能拿到 B
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // 尝试获取 B, 并没有释放 A
                synchronized (B) {
                    System.out.println("t1 拿到了两把锁!");
                }
            }
        });

        Thread t2 = new Thread(() -> {
            synchronized (A) {
                // sleep一下, 给 t1 时间, 让 t1 能拿到 A
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // 尝试获取 A, 并没有释放 B
                synchronized (B) {
                    System.out.println("t2 拿到了两把锁!");
                }
            }
        });
        t1.start();
        t2.start();
    }
}

两个线程,两把锁。线程A获取到锁A,线程B获取到锁B,在没释放锁AB的前提下,线程A尝试获取锁B,线程B尝试获取锁A,就会出现死锁。

可以把这种情况理解成:【屋钥匙锁在了车里,车钥匙锁在了屋里】

2.3、N个线程M把锁(哲学家就餐问题)

首先假设一个场景,一张圆桌上坐着五个人,每个人面前都有一碗面条,桌子上一共有五根筷子(不是五双),而将五根筷子分别摆放在两人各自之间,如下图。

        要想吃面条,需要拿起自己身旁的两根筷子(左右两根,只能拿身边的这两根)。假设此时A拿起了左右筷子吃面条,此时B就无法吃,因为A正在使用B的左筷子,B目前只能拿起一根右筷子,并且开始等待,等待A放下筷子,再拿起左筷子吃面条(此处的等待只有拿到另外一根筷子后才会停止,并且等待的同时不会放下已经拿起的筷子)。同理E也一样。

        此处讨论的问题中N等于M。我们将线程比作人筷子比作锁此时B所处的状态可以比作锁竞争引起的阻塞状态。大家可以试着想想各种其他不同的情况,始终都能保证桌上5个人至少有一人正在吃面条,除了一种特殊的极端情况下:

        极端情况下,会出现所有人同时都拿了同一侧的筷子(例如都拿了左筷子),导致所有人都不能拿起另一侧的筷子而都进入阻塞,等待着别人放下筷子后自己再拿起来。但是此时又因为没有一个人能吃的上面条,因此永远不会有人放下筷子,出现死锁。

        这个问题也被人称之为:哲学家就餐问题。

 3、解决死锁问题

要想解决死锁情况,就得先讨论产生死锁的原因:

死锁产生的四个必要条件(缺一不可)

由于是必要条件,只需要破坏其中一种条件,就可以让死锁解开。 

  1. 互斥使用。一个线程拿到了这把锁,另一个线程也想获取,就需要阻塞等待,这是锁最基本的特性,不好破坏。
  2. 不可抢占。一个线程拿到了锁之后,只能主动解锁,不能让别的线程强行把锁抢走,这也是锁最基本的特性,不好破坏。
  3. 请求保持。一个线程拿到了锁A,在持有锁A的前提下,尝试获取锁B。这些场景下必须需要这样使用,也不好破坏。
  4. 循环等待/环路等待,是一种代码结构,是最容易破坏。

由上述分析可以得知,想要解决死锁问题,要从破坏循环等待/环路等待入手。

引入加锁顺序的规则就是很好破解循环等待的办法,即给每一个锁编号,规定只能按照锁的序号顺序拿起,就能打破循环等待。

举例说明:

        依然是是上面的哲学家就餐问题,此时给筷子编号序号之后,要求只能按照顺序由小到大拿起,此时就算是所有人同时拿起筷子,C先拿1,B先拿2,A先拿3,E先拿4,此时D按照规定应该拿起1,但是此时C正拿着1,因此此时D还没有机会拿起5,就直接进入阻塞状态。此场景下E就能拿起5开始吃面,E放下筷子A就接着吃,依此类推,就将可能出现的死锁问题破解了。

 

【博主推荐】 

【Java多线程】线程中几个常见的属性以及状态-CSDN博客icon-default.png?t=N7T8https://blog.csdn.net/zzzzzhxxx/article/details/136122127?spm=1001.2014.3001.5501【Java多线程】Thread类的基本用法-CSDN博客icon-default.png?t=N7T8https://blog.csdn.net/zzzzzhxxx/article/details/136121421?spm=1001.2014.3001.5501【Java多线程】对进程与线程的理解-CSDN博客icon-default.png?t=N7T8https://blog.csdn.net/zzzzzhxxx/article/details/136115808?spm=1001.2014.3001.5501

如果觉得作者写的不错,求给博主一个大大的点赞支持一下,你们的支持是我更新的最大动力!

如果觉得作者写的不错,求给博主一个大大的点赞支持一下,你们的支持是我更新的最大动力!

如果觉得作者写的不错,求给博主一个大大的点赞支持一下,你们的支持是我更新的最大动力!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/402339.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

OpenGauss数据库本地搭建并结合内网穿透实现远程访问

文章目录 前言1. Linux 安装 openGauss2. Linux 安装cpolar3. 创建openGauss主节点端口号公网地址4. 远程连接openGauss5. 固定连接TCP公网地址6. 固定地址连接测试 前言 openGauss是一款开源关系型数据库管理系统,采用木兰宽松许可证v2发行。openGauss内核深度融合…

【AIGC】开源音频工具AudioCraft

AudioCraft是一个开源框架,旨在生成高质量的音频,适用于音乐、声音生成和压缩等多种应用。 先听效果: aimusic 它由三个模型组成:MusicGen、AudioGen和EnCodec。 MusicGen: 这个模型使用了Meta拥有和特别许可的音乐进…

如何使用Docker本地部署Jupyter+Notebook容器并结合内网穿透实现远程访问

文章目录 1. 选择与拉取镜像2. 创建容器3. 访问Jupyter工作台4. 远程访问Jupyter工作台4.1 内网穿透工具安装4.2 创建远程连接公网地址4.3 使用固定二级子域名地址远程访问 本文主要介绍如何在Ubuntu系统中使用Docker本地部署Jupyter Notebook,并结合cpolar内网穿透…

UE4 C++联网RPC教程笔记(三)(第8~9集)完结

UE4 C联网RPC教程笔记(三)(第8~9集)完结 8. exe 后缀实现监听服务器9. C 实现监听服务器 8. exe 后缀实现监听服务器 前面我们通过蓝图节点实现了局域网连接的功能,实际上我们还可以给项目打包后生成的 .exe 文件创建…

edge安装fdm插件

下载 https://www.crxsoso.com/webstore/detail/ahmpjcflkgiildlgicmcieglgoilbfdp 安装 进入edge插件管理页面 edge://extensions/2. 将下载的crt文件拖到这个页面,就能自动安装了 在其他网页不能安装,会变成下载。

2024年noc比赛Coding创意编程赛项-创意实验室初赛模拟题

【单选题】 1.角色本来面向的方向是右方,执行下方积木后,角色面向的方向是() A.面向右上方 C.面向左上方 B.面向右下方 D.面向左下方 2.下列选项中关于图中按钮功能说法错误的是() A."本地传”按钮可以从本地电脑上传素材 B."重新画”按钮可以自己设计素材 C"…

QT的UI入门

二、UI入门 QWidget类(熟悉) QWidget类是所有组件和窗口的基类,内部包含了一些基础的界面特性。 常用属性: 修改坐标 x : const int 横坐标,每个图形的左上角为定位点,横轴的零点在屏幕的最左边&#xff0c…

Javase-方法的使用

文章目录 一 . 方法的初步认识二 . 方法的定义三 . 方法调用的执行过程四 . 实参与形参的关系五 . 方法的重载 一 . 方法的初步认识 方法其实就是一些代码片段,类似于c语言中的函数 方法存在的意义(理解): 是能够模块化的组织代码(当代码规模比较复杂的时候).做到代码被重复使…

一文搞懂match、match_phrase与match_phrase_prefix的检索过程

一、在开始之前,完成数据准备: # 创建映射 PUT /tehero_index {"settings": {"index": {"number_of_shards": 1,"number_of_replicas": 1}},"mappings": {"_doc": {"dynamic": …

[计算机网络]---TCP协议

前言 作者:小蜗牛向前冲 名言:我可以接受失败,但我不能接受放弃 如果觉的博主的文章还不错的话,还请点赞,收藏,关注👀支持博主。如果发现有问题的地方欢迎❀大家在评论区指正 目录 一 、TCP协…

如何在群辉7.2中使用Docker搭建容器魔方服务并远程访问【内网穿透】

文章目录 1. 拉取容器魔方镜像2. 运行容器魔方3. 本地访问容器魔方4. 群辉安装Cpolar5. 配置容器魔方远程地址6. 远程访问测试7. 固定公网地址 本文主要介绍如何在群辉7.2版本中使用Docker安装容器魔方,并结合Cpolar内网穿透工具实现远程访问本地网心云容器魔方界面…

深入了解Git

1.1 Git 的工作流程简介 克隆 Git 资源作为工作目录 在克隆的资源上添加或修改文件 如果其他人修改了,你可以更新资源 在提交前查看修改 提交修改 在修改完成后,如果发现错误,可以撤回提交并再次修改并提交 1.2 Git 工作区、暂存区和版…

2-21算法习题总结

由于蓝桥杯的题,我不知道从怎么复制,就只能粘贴图片了 翻硬币 代码 import java.util.Scanner;public class Main {public static void main(String[] args) {Scanner sc new Scanner(System.in);String start sc.next();char[] starts start.toCharArray();String end sc…

nginx优化配置

一 全局配置的六个模块简介 全局块:全局配置,对全局生效 events块:配置影响 Nginx 服务器与用户的网络连接 http块:配置代理,缓存,日志定义等绝大多数功能和第三方模块的配置 server块:配置…

TypeScript01:安装TypeScript

一、TypeScript 官方网站:https://www.tslang.cn/docs/index.html 练习场:https://www.typescriptlang.org/zh/play 好处: 强类型语言,对JS弱类型的一个良好补充;TS利于大型项目团队合作,可以一定程度…

大工程 从0到1 数据治理 数仓篇(sample database classicmodels _No.7)

大工程 从0到1 数据治理 之数仓篇 我这里还是sample database classicmodels为案列,可以下载,我看 网上还没有类似的 案列,那就 从 0-1开始吧! 提示:写完文章后,目录可以自动生成,如何生成可参…

WordPress后台自定义登录和管理页面插件Admin Customizer

WordPress默认的后台登录页面和管理员,很多站长都想去掉或修改一些自己不喜欢的功能,比如登录页和管理页的主题样式、后台左侧菜单栏的某些菜单、仪表盘的一些功能、后台页眉页脚某些小细节等等。这里boke112百科推荐这款可以让我们轻松自定义后台登录页…

2024-02-21 学习笔记(DETR)

自动多模态检测验证效果不佳(过检太多)后,节后开始尝试DETR路线。 基本梳理了下DETR发展和验证的脉络,先进行相应指定场景的效果验证。 关于DETR系列的介绍,B站上比较多,迪哥的都讲的比较细。 推荐大佬的…

【数据结构】_队列

目录 1.概念 2.队列的使用 3.队列模拟实现 4.循环队列 5.双端队列 6.栈与队列的互相实现 6.1 用队列实现栈 6.2 用栈实现队列 1.概念 (1)队列是只允许在一端进行插入数据操作,在另一端进行删除数据操作的特殊线性表; &am…