Java多线程详解⑥(全程干货!!!)死锁 || 构成死锁的条件 || 死锁总结 || Java标准库的线程安全类

这里是Themberfue

· 在上一节中,我们介绍了线程安全问题,对锁的概念以及使用

· 在本节中,进入 "死锁" 的概念以及如何产生 "死锁"


死锁

一个线程,一把锁,同时加两把锁

· 要想进入死锁的介绍和概念,我们先来看这段代码

public class Demo18 {private static int count = 0;public static void main(String[] args) throws InterruptedException {Object locker = new Object();Thread t1 = new Thread(() -> {for (int i = 0; i < 50000; i++) {synchronized (locker) {synchronized (locker) {count++;}}}});t1.start();t1.join();System.out.println("count = " + count);}
}

 · 上述代码对 count++ 加了两次锁,看起来这样写没必要,也感觉根本写不出这样的代码,但实际在开发过程还是非常容易写到的

class Counter {int count;synchronized public void addCount() {synchronized (this) {this.count++;}}
}Counter count = new Counter();synchronized (locker) {count.addCount();
}

· 万一是在这种情况,addCount方法被 synchronized 修饰,但是 addCount 方法可能被封装了,你并不清楚其被 synchronized 修饰 

· 我们想来分析上述代码,为什么会出现问题:

· 首先,进入第一个 synchronized 代码块,也就是进入第一把锁

· 随后,进入第二把锁,但是根据锁的互斥,由于该锁已被加上,要想继续加锁,就得等第一把锁解开后

· 但是,想要解开第一把锁,就得执行完此处逻辑,导致代码在这卡住不能继续执行下去

· 没错,这就是这里的问题

· 上述类似这样的问题,就称为 "死锁(dead lock)"

· 但是,运行代码后,并没有出现代码卡住的问题,而是继续往下运行并且正常结束了

· 为了解决这个问题,Java 的 synchronized 引入了 "可重入"  的概念,对于这种二次加同样锁的情况,JVM会自动消除,使得代码正常执行

· 具体过程就是,JVM会判断当前加的锁是否在当前线程中已经加过,如果已经加过了,此时不会阻塞等待,而是直接往下运行

· 这也是 Java 的好处之一,例如 C++ 的锁并没有这样的功能,需要程序员额外小心这样的情况发生

PS:JVM是怎么实现这一功能的?用个计数器,计下 ' { ' 的数量,随后想要了解更多,网上有很多答案哦~~~🫡


两个线程,两把锁,两个线程获取到锁后,尝试获取对方的锁

· 先举个例子:你和朋友去吃火锅,你这边拿着麻辣酱,他那边拿着酱油,你此时想要想要蘸那边的酱油,但与此同时,他又想要蘸你这边的麻辣酱,但他不肯放下酱油,你也不肯放下麻辣酱,此时你俩就僵住了,谁都吃不到了,卡住了属于是

· 再举个例子:家钥匙锁车里了,车钥匙锁家里了

public class Demo19 {public static void main(String[] args) throws InterruptedException {Object locker1 = new Object();Object locker2 = new Object();Thread t1 = new Thread(() -> {synchronized (locker1) {try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}// t1获取t2的锁synchronized (locker2) {System.out.println("t1获取到t2的锁");}}});Thread t2 = new Thread(() -> {synchronized (locker2) {try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}// t2获取t1的锁synchronized (locker1) {System.out.println("t2获取到t1的锁");}}});t1.start();t2.start();t1.join();t2.join();}
}

· 来看上述代码:

        由于线程是并发执行的,t1 想要获取到 t2 的锁,此时进入阻塞状态 

        t2 想要获取到 t1 的锁,此时也进入阻塞状态

        在这种情况,t1 不让 t2,t2 不让 t1,那么就进入了僵持状态,也就是造成了 "死锁" 

· 但如果给某个线程 sleep 一会儿,那么此时可能会 死锁,也可能不会 死锁

· 运行代码,我们打开 jconsole 查看其状态

 

· 可以看到,两个线程的状态都是 BIOCKED 状态,也就是死等状态,死锁属于了

· 如何解决呢?如果必须要互相拿到对方的锁的话,那么就不要嵌套解锁,而是并列加锁

public class Demo20 {public static void main(String[] args) throws InterruptedException {Object locker1 = new Object();Object locker2 = new Object();Thread t1 = new Thread(() -> {synchronized (locker1) {try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}// t1获取t2的锁synchronized (locker2) {System.out.println("t1获取到t2的锁");}});Thread t2 = new Thread(() -> {synchronized (locker2) {try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}// t2获取t1的锁synchronized (locker1) {System.out.println("t2获取到t1的锁");}});// 把嵌套的加锁操作改为并列加锁,可以有效解决死锁的问题// 请求和保持t1.start();t2.start();t1.join();t2.join();}
}

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

· 现有五个哲学家,五根筷子,一张桌子,五碗面条,此时,五位哲学家都想吃面,他们同时拿起筷子,此时一人分得一把筷子,但是一根筷子怎么吃面条啊?所以,至少需要两根筷子才可以夹起面条

· 但是,哲学家是固执的,此时,他们谁也不让谁,谁也别想拿到第二根筷子,那么就又僵住了

· 如果将五个哲学家类比为五个线程,五跟筷子类比为五个锁,只有拿到两把锁,该线程才可以继续工作,但问题就在于,每个线程都拿不到第二把锁了,那么此时程序就又双 "死锁" 了

· 此时,有人会说:啊,这个问题几率那么小,根本没必要在意啊。确实,这个问题产生的概率确实不大,但如果用户基数大了,那么发生的概率也会变大,那么发生的概率是 1%,基数大了,发生的概率也就大了,这种小概率事件也必须得到有效解决

 此图转载自 Wiki

 · 那么应该如何解决呢?

        1. 我们给五根筷子按顺序命名

        2. 五位哲学家按照约定拿起筷子:永远拿起序号更小的筷子,随后再获取序号更大的筷子

        那么就是:从第二位哲学家开始,他先拿起序号更小的筷子,也就是 1 号筷子;第三位哲学家拿起 2 号筷子;第四位哲学家拿起 3 号筷子;第五位哲学家拿起 4 号筷子。

        3. 此时,第一位哲学家想要拿起筷子,但是按照约定,必须拿起序号更小的筷子,所以,他不能拿起 5 号筷子,那么他现在就啥也不能干了

        4. 现在 5 号筷子处于空闲状态,那么第五位哲学家就可以拿起这 5 号筷子,那么第五位哲学家就拿起了两把筷子,就可以吃面条了

        5. 第五位哲学家吃完面条了,满足了,放下了 4 号和 5 号筷子,此时,第四位哲学家看到了 4 号筷子空闲,那么,他拿起 4 号筷子,也可以吃面条了

        6. 我四位哲学家吃完面条,吃饱了哦,那么他也放下了 3 号和 4 号筷子,同时,第三位哲学家也看到了 3 号筷子空闲了,他就拿起了 3 号筷子,可以干饭了

        7.以此类推,三号哲学家干饱了,二号哲学家干饱了,放下了 1 号筷子

        8. 此时,第二位哲学家,第三位哲学家,第四位哲学家,第五位哲学家都吃饱了,全部筷子也处于空闲状态了。也就轮到第一位哲学家了,按照约定,先拿起序号小的筷子后拿起序号大的筷子,所以他就拿起 1 号筷子和 5 号筷子了

        9. 这样,五位哲学家都吃饱了,问题也就随之解决了~~~


构成死锁的条件

1. 锁是互斥的

        一个线程拿到锁后,另一个线程想要尝试拿到这个锁,此时会进入阻塞等待状态

2. 锁是不可抢占(不可剥夺)的

        线程1 拿到锁后,线程2 再次尝试拿到这个锁,必须会进入阻塞等待,而不是直接抢走线程1 的锁

3. 请求和保持

        线程 1 在拿到锁后,接着尝试拿到线程 2拿到的锁,同时,线程 2在拿到锁时,接着尝试拿到线程 1拿到的锁应该在线程 1放下拿到的这个锁后,在尝试拿起线程 2的锁,同理线程 2也是如此,也就是避免 锁嵌套 ,但是在某些场景下不可能避免地嵌套锁

4. 循环等待

        线程 A 等待 线程 B ,线程 B 等待 线程 C ,线程 C 等待 线程 D,线程 D 等待 线程 A

        约定好加锁的顺序就可以解决循环等待

· 上述 1 2 情况是 锁 自带的性质和特性,是不可避免的,但是 3 4 情况是可以尽量避免的


死锁总结 

构成死锁的三个场景

        1. 一个线程同时加两次相同的锁(可重入锁)

        2. 两个线程两把锁(代码如何编写)

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

构成死锁的条件

        1. 互斥

        2. 不可抢占

        3. 请求和保持

        4. 循环等待

避免死锁

        3. --> 避免嵌套锁

        4. --> 按照顺序加锁


Java标准库的线程安全类

        · 类似于:ArrayList,LinkedList,HashMap,TreeMap,HashSet,TreeSet,StringBuilder类都是线程不安全

        这些类没有任何有关加锁限制的代码

        · 但是,类似于 Vector,HashTable,ConcurrentHashMap,StringBuffer类都是线程安全的

        通过查看源代码可以知道,他们的提供的方法都是有 synchronized 修饰的

        · 众所周知,有失必有得,我们加了锁也不一定就是好的 --> 加了锁意味着可能会出现锁竞争,出现锁竞争就意味着出现阻塞等待,这程序的执行效率是大大折扣的

        · 所以,加锁这件事,并不是一定要加,根据场景合理加锁才能写出好的多线程代码

        · String 类是比较特殊的类,因为其被 final 修饰,所以它是不可更改的,固然其线程天然安全

· 下一节我们将了解到内存可见性以及指令重排序等一系列多线程更多的知识,敬请期待哦~

· 毕竟不知后事如何,且听下回分解~~

 

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

适用于 Windows 11/10 电脑 的 13 个最佳文件恢复软件

如果您由于系统故障、硬件损坏、人为错误或病毒攻击而丢失了重要文件或文件夹。不用担心&#xff0c;因为我们随时为您提供帮助&#xff01;借助正确的文件恢复工具&#xff0c;您可以立即检索计算机上不同类型的文件。如果你有为您的文件创建备份&#xff0c;你不用担心&#…

> 甘晴void:课程资源免费下载

为进一步减少信息差&#xff0c;便利同学&#xff0c;我把部分重要课程的轨迹&#xff08;期中期末考卷、机考题、作业答案、代码、工程项目等&#xff09;上传至Github。网址链接如下&#xff1a; https://github.com/wolfvoid/HNU-resourses Github平台的优势是下载免费&am…

华为OD机试 - 求小球落地5次后所经历的路程和第5次反弹的高度 (Java 2024 E卷 100分)

华为OD机试 2024E卷题库疯狂收录中&#xff0c;刷题 点这里。 实战项目访问&#xff1a;http://javapub.net.cn/ 专栏导读 本专栏收录于 《华为OD机试&#xff08;JAVA&#xff09;真题&#xff08;E卷D卷A卷B卷C卷&#xff09;》 。 刷的越多&#xff0c;抽中的概率越大&…

uniapp—android原生插件开发(2原生插件开发)

本篇文章从实战角度出发&#xff0c;将UniApp集成新大陆PDA设备RFID的全过程分为四部曲&#xff0c;涵盖环境搭建、插件开发、AAR打包、项目引入和功能调试。通过这份教程&#xff0c;轻松应对安卓原生插件开发与打包需求&#xff01; ***环境问题移步至&#xff1a;uniapp—an…

【机器学习】强化学习(1)——强化学习原理浅析(区分强化学习、监督学习和启发式算法)

文章目录 强化学习介绍强化学习和监督学习比较监督学习强化学习 强化学习的数学和过程表达动作空间序列决策策略&#xff08;policy&#xff09;价值函数&#xff08;value function&#xff09;模型&#xff08;model&#xff09; 强化学习和启发式算法比较强化学习步骤代码走…

常见 HTTP 状态码分类和解释及服务端向前端返回响应时的最完整格式

目前开发的项目很大程度上是为明年的国产化做准备了&#xff0c;所以借这个机会把用了十年的自研系统全部重写&#xff0c;订立更严格的规范&#xff0c;本文记录一下返回格式及对应状态码。 常见 HTTP 状态码及解释 HTTP 状态码用于表示客户端请求的响应状态&#xff0c;它们…

深入理解JVM

文章目录 1. JVM内存区域划分2. JVM中类加载过程类加载&#xff08;1&#xff09;类加载的基本流程&#xff08;2&#xff09;双亲委派模型 《深入理解java虚拟机》 在这本书前&#xff0c;面试官对于JVM也不是很了解。 这本书主要还是写个一下开发 JVM 的人。 1. JVM内存区域…

启动软件是用例吗

沙亚雄 2020-8-13 14:06 潘老师&#xff0c;你好&#xff0c;最近读了您的软件方法&#xff0c;结合自己的项目发现始终有一个疑问&#xff1f;那就是启动软件算不算一个用例&#xff0c;因为软件在启动的时候一般都要实例化一些对象&#xff0c;读一些配置文件等等。那启动软…

Ubuntu22.04中使用CMake配置运行boost库示例程序

Ubuntu22.04中使用CMake配置运行boost库示例程序 boost是一个比较强大的C准标准库&#xff0c;里面有很多值得学习的东西&#xff0c;比较asio网络库可以用来编写C TCP客户端或者TCP服务端接收程序。本文主要讲解如何在Ubuntu22.04中使用Cmake配置boost库&#xff0c;以及运行…

指标+AI+BI:构建数据分析新范式丨2024袋鼠云秋季发布会回顾

10月30日&#xff0c;袋鼠云成功举办了以“AI驱动&#xff0c;数智未来”为主题的2024年秋季发布会。大会深度探讨了如何凭借 AI 实现新的飞跃&#xff0c;重塑企业的经营管理方式&#xff0c;加速数智化进程。 作为大会的重要环节之一&#xff0c;袋鼠云数栈产品经理潮汐带来了…

Windows配置NTP时间同步

Windows下实现NTP时间同步 1、Windows时间服务(W32Time)2、Windows 时间同步的工作原理3、配置和管理 Windows 时间同步3.1 命令行工具&#xff1a;w32tm3.2 控制面板中的设置 4. 高级设置&#xff08;Windows Server 环境&#xff09;5.调整时间同步的间隔5.1 通过组策略调整时…

系统安全第七次作业题目及答案

一、 1.RBAC0 RBAC1 RBAC2 RBAC3 2.属性 身份标识 3.接入访问控制 资源访问控制 网络端口和节点的访问控制 二、 1.B 2.A 3.ABE 4.BCD 5.ABC 三、 1. 答&#xff1a;基于属性的访问控制&#xff08;ABAC&#xff09;是通过对实体属性添加约束策略的方式实现主、客体之…

Golang进阶

1.面向对象 1.1.golang语言面向对象编程说明 Golang 也支持面向对象编程(OOP)&#xff0c;但是和传统的面向对象编程有区别&#xff0c;并不是纯粹的面向对象语言。所以我们说 Golang 支持面向对象编程特性是比较准确的。Golang 没有类(class)&#xff0c;Go 语言的结构体(st…

Node(节点)、Menu(菜单) 和 Tab(标签页)之间的关系

在系统开发中&#xff0c;尤其是在涉及到前端界面设计和后台管理系统时&#xff0c;我们经常会看到 Node&#xff08;节点&#xff09;、Menu&#xff08;菜单&#xff09; 和 Tab&#xff08;标签页&#xff09; 这几个概念。这些概念有不同的用途和功能&#xff0c;理解它们之…

Python数据分析案例64——杭帮菜美食探索数据分析可视化

案例背景 杭州是真没啥美食呀.....但是 总是还是有好吃的店家&#xff0c;于是就发挥专业长处&#xff0c;进行一下分析&#xff0c;看看杭帮菜的一些特点。。例如看看品种分布啊&#xff0c;类型分布啊&#xff0c;行政区的分布啊&#xff0c;店铺评分的一些分布啊&#xff0…

基于SSD模型的路面坑洼检测系统,支持图像、视频和摄像实时检测【pytorch框架、python源码】

更多目标检测和图像分类识别项目可看我主页其他文章 功能演示&#xff1a; 基于SSD模型的路面坑洼检测系统&#xff0c;支持图像、视频和摄像实时检测【pytorch框架、python源码】_哔哩哔哩_bilibili &#xff08;一&#xff09;简介 基于SSD模型的路面坑洼检测系统是在 Py…

《Python编程实训快速上手》第四天--字符串操作

一、处理字符串 1、单引号和双引号 Python中单双引号均可以表示字符串&#xff0c;区别在于&#xff1a; 1、双引号中可以使用到单引号 2、单引号字符串中如果要使用单引号&#xff0c;要使用到转义字符 \ \ \t \n \\ 原始字符串 在开始的引号前加r&#xf…

泷羽sec学习打卡-Windows基础命令

声明 学习视频来自B站UP主 泷羽sec,如涉及侵权马上删除文章 笔记的只是方便各位师傅学习知识,以下网站只涉及学习内容,其他的都与本人无关,切莫逾越法律红线,否则后果自负 关于windows的那些事儿-Base 一、Windows-BaseWindows有哪些版本呢&#xff0c;有什么区别呢&#xff1f…

Node.js——fs模块-文件夹操作

1、借助Node.js的能力&#xff0c;我们可以对文件夹进行创建、读取、删除等操作 2、方法 方法 说明 mkdir/mkdirSync 创建文件夹 readdir/readdirSync 读取文件夹 rmdir/rmdirSync 删除文件夹 3、语法 其余的方法语法类似 本文的分享到此结束&#xff0c;欢迎大家评论区…

VMware Fusion和centos 8的安装

资源 本文用到的文件&#xff1a;centos8镜像 , VMware 软件包 , Termius 文件链接: https://pan.baidu.com/s/1kOES_ZJ8NGN-BnJl6NC7Sg?pwd63ct 安装虚拟机 先 安装 vmware &#xff0c;然后打开&#xff0c;将下载的 iso 镜像拖入 拖入镜像文件iso Continue, 然后随便选…