【智能解析多线程:线程安全与死锁的深度剖析】

](https://img-home.csdnimg.cn/images/20220524100510.png#pic_center)
🌈个人主页: Aileen_0v0
🔥热门专栏: 华为鸿蒙系统学习|计算机网络|数据结构与算法
💫个人格言:“没有罗马,那就自己创造罗马~”

文章目录

    • 温故而知新
    • 线程安全问题
    • 多线程中有的线程未加锁
    • 一个线程有多把锁
    • 加了多层锁的代码,执行到最后一个 } 才真正解锁
    • 死锁三大场景
      • 场景1: 非可重入锁 ——(多套几个锁即可解决死锁)
      • 场景2: 两个线程两把锁【你在等我,我也在等你,不主动就永远没有故事直到死亡~】—— (通过jconsole调用栈+状态定位解决)
      • 场景3:N个线程,M把锁
    • 死锁
    • ⭐️⭐️⭐️⭐️⭐️死锁的四个必要条件【缺一不可,任何一个死锁的场景都必须同时具备这四个条件,少一个都不会发生死锁。】
      • 从第三点出发解决死锁问题
      • 从第四点出发解决死锁问题(简单且高效的方法)
    • 多个线程获取多把锁避免死锁的解决办法

温故而知新

在这里插入图片描述

线程安全问题

在这里插入图片描述
在这里插入图片描述

对比上面的两段代码,我们可以看到,当我们i的的值 循环次数较少时,发生线程安全的问题明显就减小了,但它任然存在线程安全问题。

上面代码中当i很小时,t1就开始计算了,可能会出现:t2还没启动,t1就运行完了,此时这两个线程就相当于是串行执行。

通过对比上面两段多线程代码的运行结果:我们可以得出多线程代码运行具有随机性。

package thread;

class Counter2{
    private  int count = 0 ;
    void add(){
        count++;
    }
    int get(){
        return  count;
    }
}
public class Demo21 {
    public static void main(String[] args) throws InterruptedException {
        Counter2  counter2 = new Counter2();
        Thread t1 = new Thread(() -> {
            for(int i = 0 ; i < 5000 ; i++){
                synchronized (counter2){
                    counter2.add();
                }
            }
        });
        Thread t2 = new Thread(() -> {
            for(int i = 0 ; i < 5000 ; i++){
                synchronized (counter2){
                    counter2.add();
                }
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("count = " + counter2.get());

    }
}

在这里插入图片描述

如果一个线程加了锁另一个线程不加锁,那线程还安全吗?

多线程中有的线程未加锁

在这里插入图片描述](https://i-blog.csdnimg.cn/direct/5f6db2e1208d4f868fe44b0abf1d3253.png)

多线程中如果有的线程未加锁,会发生线程不安全的情况,上面代码中t2未加锁,即使t1加锁了,由于t2没有任何阻塞,没有互斥,任然会使t1++到一半的时候,被t2进来把结果覆盖掉。

故事时间:上面的就好比两个男的追一个女的,但是这个女的和其它男的谈了,但是这两个男的依旧是穷追不舍,其中一个呢就比较老实一点,默默观望;另一个就是霸王硬上弓,恨不得打一架

一个线程有多把锁

在这里插入图片描述

假设t2先启动(t1先不考虑),t2线程第一次加锁肯定能成功,但当t2尝试第二次加锁时,此时counter2变量,属于已被锁定的状态,根据之前的知识,当我们针对一个已被锁定的线程加锁就会出现阻塞等待,并且会一直阻塞到对象被解锁时。

想要获取第二把锁,就需要先给第一把锁解锁,但是想要给第一把锁解锁就需要执行完第一层大括号,但执行完第一层大括号又需要先获取第二层锁,这两层加锁解锁操作相互矛盾,这种情况就叫做“死锁”。

但是根据上面的运行结果,我们可以看到,这个结果是正确的,也就是说上述死锁过程对于“synchronized”并不适用,但是对于C++/python 就会出现死锁的现象。

synchronized 之所以没出现上面死锁现象的原因,是因为自己在内部(JVM)做了特殊处理——每个锁对象里会记录当前哪个线程持有了这个锁,如果下次再加锁,发现他是针对同一个线程加锁就不会去调用操纵系统的阻塞加锁了,这就可以避免出现死锁的现象。(tip:两把锁可以开同一个开一个门,我只要其中一把即可,另一把可忽略使用)

加了多层锁的代码,执行到最后一个 } 才真正解锁

在这里插入图片描述

但是如果有N层锁,如何判定这个 } 是最外层的 } ,JVM如何识别?
  • 解决方案⚠️:“引用计数器”:在锁对象里面维护一个计数器(int n):
    • 每次遇到 { ,n 就 ++ (只有第一次才真正加锁)
    • 每次遇到 } , n 就-- (当 n 减到0了,才真正解锁)
    • 也就是说里层加锁是虚而不实的,一般来说,只需要在外层加锁即可。【之所以要在里层加锁,是为了防止出现死锁,这种机制叫 “可重入锁”】

死锁三大场景

在这里插入图片描述

场景1: 非可重入锁 ——(多套几个锁即可解决死锁)

场景2: 两个线程两把锁【你在等我,我也在等你,不主动就永远没有故事直到死亡~】—— (通过jconsole调用栈+状态定位解决)

  • 现有线程1和线程2,以及有锁A和B,现在线程1和线程2都需要获取锁A和锁B,但是拿到锁A之后,不释放A,继续获取锁B

故事时间:疫情期间,有一个地方的健康码崩了,程序员赶紧回公司去修复Bug,但他在楼下被保安拦住要求出示健康码才能上楼,但是程序员如果没有上楼修复好Bug就无法出示健康码,如果两个人你不让我我不让你,就会一直僵持,这就是死锁。

  • 代码示例:
package thread;

public class Demo22 {
    public static void main(String[] args) throws InterruptedException {
        Object locker1 = new Object();
        Object locker2 = new Object();

        Thread t1 = new Thread(() -> {
            synchronized(locker1){
                //为了更好的控制线程的执行顺序,引入sleep,否则死锁可能会重现不出来
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }

                synchronized(locker2){
                    System.out.println("t1 获取两把锁");
                }
            }
        });

        Thread t2 = new Thread(() -> {
            synchronized(locker2){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }

                synchronized(locker1){
                    System.out.println("t2 线程拿到两把锁");
                }
            }
        });

        t1.start();
        t2.start();

        t1.join();
        t2.join();
    }
}

在这里插入图片描述

  • 上面代码中,先分别让t1和t2拿到一把锁,然后再尝试去获取对方的锁。
    在这里插入图片描述

  • 根据上面的运行结果,我们可以看到进程并未退出,也未打印线程中的内容,这就是死锁现象。

在这里插入图片描述

在这里插入图片描述

  • 上图是在jconsole找到阻塞的具体代码位置。并且在Java的锁对象中会记录当前是哪个线程持有这把锁。

  • 如果只有一个线程,就不会触发锁竞争,也就不会发生阻塞现象。


场景3:N个线程,M把锁

  • 随着线程数目/锁个数的增加,此时,情况更加复杂,就更容易出现死锁。

    • 案例:哲学家问题
      在这里插入图片描述
  • 每个滑稽都坐在每个筷子之间,每个滑稽都要做两件事情:

    • 1.思考人生(放下筷子)
    • 2.吃面条(需要拿起左右两根筷子)
  • 每个哲学家啥时候吃面条,啥时候思考人生都是不确定的(抢占式执行)

  • 上面的模型,大部分可正常工作,但是如果出现极端情况就会出现问题:

  • 同一时刻,所有滑稽老铁,都拿起左边的筷子,此时,所有滑稽老铁都无法拿起右边的筷子,并且每个滑稽都是比较固执的人,每个哲学家只要吃不到面条,就不会放下手中的筷子。

  • 上面就是典型的死锁状态,更多的哲学家,更多的筷子,情况也类似


死锁

  • 死锁:死锁是非常严重的问题,他会使线程被卡主,无法继续工作,死锁这种Bug的出现具有概率性,测试的时候啥事没有,但是一发布就出问题,即使一发布了没问题,等到大家睡着了说不定又出现Bug。

⭐️⭐️⭐️⭐️⭐️死锁的四个必要条件【缺一不可,任何一个死锁的场景都必须同时具备这四个条件,少一个都不会发生死锁。】

  • 1.锁具有互斥性 (基本特点:一个线程拿到锁以后,其他线程就阻塞等待)
  • 2.锁不可抢占(不可被剥夺)一个线程拿到锁以后,除非它自己主动选择释放锁,否则别人抢不走(锁的基本特点)
  • 3.请求和保持,一个线程拿到一把锁以后,不释放这个锁的情况下,再尝试去获取其它锁。嵌套锁【代码结构】
  • 4.循环等待。(多个线程获取多个锁的过程中,出现了循环等待,A等待B,B又等待A)你等我我等你,谁都不主动的死等【代码结构】

⚠️注意:如果是自己实现的锁,就可以实现打破互斥,打破不可剥夺这两个条件,但是对于synchronized这样的锁就不行。

从第三点出发解决死锁问题

  • 我们将刚刚的死锁代码进行修改,根据第三点既然嵌套会发生死锁,那我们可以将这两个锁分开,不让它们嵌套,就可以解决死锁问题
    在这里插入图片描述
    在这里插入图片描述

从第四点出发解决死锁问题(简单且高效的方法)

  • 第四点就是你等我,我等你,互不相让造成的死锁,要解决这个问题,就需要破除循环等待,(双向奔赴)约定好加锁顺序,让所有线程按照固定顺序来获取锁,这样即使出现第三点的嵌套也不会产生死锁现象。eg:约定必需先获取 locker1 后获取 locker2 。
    在这里插入图片描述

在这里插入图片描述

  • 上面的代码中约定了完成 t1 执行的逻辑以后,释放完 locker1 之后,才轮到 t2 执行

多个线程获取多把锁避免死锁的解决办法

  • ⭐️⭐️⭐️⭐️⭐️当代码中,需要用到多个线程获取多把锁,一定要约定好加锁顺序,可以有效避免死锁。如下所示:

在这里插入图片描述

  • 上图中滑稽老铁就餐问题,我们约定每个滑稽必需从编号小的筷子开始获取,然后才能获取编号大的;从2号老铁开始拿筷子,每人左手拿一只,但是一号老铁非要拿1号的筷子,这就会导致,1和2老铁之间发生线程阻塞;为了解决这个问题,我们可以做以下操作:
  • 先让5号老铁用4号和5号筷子,等他吃完释放筷子以后,4号老铁就可以拿起4号和3号筷子,4号吃完释放以后,3号老铁就可以拿起3号和2号筷子吃面,吃完释放,2号就可以拿起2号和1号筷子,吃完释放后,1号老铁就能拿起1号和5号筷子去吃面条了,这就能保证每个老铁都能根据自己的需求拿到对应号码的筷子去吃面条,避免阻塞等待。

](https://img-home.csdnimg.cn/images/20220524100510.png#pic_center)
](https://img-home.csdnimg.cn/images/20220524100510.png#pic_center)

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

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

相关文章

Grafana系列之面板接入Prometheus Alertmanager

关于Grafana的仪表板Dashboard&#xff0c;以及面板Panel&#xff0c;参考Grafana系列之Dashboard。可以直接在面板上创建Alert&#xff0c;即所谓的Grafana Alert&#xff0c;参考Grafana系列之Grafana Alert。除了Grafana Alert外&#xff0c;面板也可接入Prometheus Alertma…

【深度学习入门】深度学习知识点总结

一、卷积 &#xff08;1&#xff09;什么是卷积 定义&#xff1a;特征图的局部与卷积核做内积的操作。 作用&#xff1a;① 广泛应用于图像处理领域。卷积操作可以提取图片中的特征&#xff0c;低层的卷积层提取局部特征&#xff0c;如&#xff1a;边缘、线条、角。 ② 高层…

数据结构 链表2

目录 前言&#xff1a; 一&#xff0c;反转一个链表(迭代) 二&#xff0c;打印一个链表&#xff08;递归&#xff09; 三&#xff0c;反转一个链表(递归) 四&#xff0c;双向链表 总结 前言&#xff1a; 我们根据 [文章 链表1] 可以知道链表相比较于数组的优缺点和计算机…

curl简介与libcurl开源库的使用总结

curl工具和libcurl不是同一个东西&#xff0c;二者的关系主要体现在以下方面&#xff1a; 定义与性质 curl工具&#xff1a; 是一个利用URL语法在命令行下工作的文件传输工具&#xff0c;1997年首次发行。它支持多种协议&#xff0c;如HTTP、HTTPS、FTP、FTPS等&#xff0c;可用…

node.js 07.npm下包慢的问题与nrm的使用

一.npm下包慢 因为npm i 默认从npm官网服务器进行下包,但是npm官网服务器是海外服务器所以响应很慢. 于是我们通过npm下包的时候通常用淘宝镜像进行下包,下面是切换到淘宝镜像地址下包的操作. 二.nrm的使用 nrm是一个管理切换npm下包地址的工具,可以快速切换下包的地址. 安…

Flutter:carousel_slider 横向轮播图、垂直轮播公告栏实现

安装依赖 carousel_slider: ^5.0.01、垂直滚动公告栏 import package:carousel_slider/carousel_options.dart;// 垂直滚动公告栏Widget _buildNotice() {return <Widget>[<Widget>[TDImage(assetUrl: "assets/img/home11.png",width: 60.w,height: 60.w…

RavenMarket:用AI和区块链重塑预测市场

不论是美股市场还是加密市场&#xff0c;AI都是本轮周期里的最大叙事。本轮AI的最大受益者英伟达市值超越苹果一跃成为全球第一大公司&#xff0c;加密领域围绕着AI的创新也是层出不穷&#xff0c;很多项目方开始向着AI转型。 而近期币圈最热门的板块就是AI agent&#xff0c;…

【玩转全栈】----Django基本配置和介绍

目录 Django基本介绍&#xff1a; Django基本配置&#xff1a; 安装Django 创建项目 创建app 注册app Django配置路由URL Django创建视图 启动项目 Django基本介绍&#xff1a; Django是一个开源的、基于Python的高级Web框架&#xff0c;旨在以快速、简洁的方式构建高质量的Web…

学技术学英语:TCP的三次握手和四次挥手

单词 汉语意思 音标 acknowledge 承认&#xff0c;确认 /əkˈnɒl.ɪdʒ/ acknowledgment 确认&#xff0c;承认 /əkˈnɒl.ɪdʒ.mənt/ duplex 双向的 /ˈdjuː.pleks/ establish 建立 /ɪˈstb.lɪʃ/ handshake 握手&#xff0c;握手协议 /ˈhnd.ʃeɪk…

iconfont等图标托管网站上传svg显示未轮廓化解决办法

打开即时设计 即时设计 - 可实时协作的专业 UI 设计工具 导入图标后拖入画板里面&#xff0c;右键选择轮廓化 将图标导出

SpringBoot集成Flink-CDC,实现对数据库数据的监听

一、什么是 CDC &#xff1f; CDC 是Change Data Capture&#xff08;变更数据获取&#xff09;的简称。 核心思想是&#xff0c;监测并捕获数据库的变动&#xff08;包括数据或数据表的插入、 更新以及删除等&#xff09;&#xff0c;将这些变更按发生的顺序完整记录下来&…

豆包 MarsCode + 开源 = ?AI 助力开源社区新人成长

来源&#xff5c;豆包 MarsCode “开源” 这个词&#xff0c;对开发者来说&#xff0c;可能是入门时的第一步&#xff0c;也可能是追求极致技术的终点。无数优秀的开源项目不仅推动了技术的进步&#xff0c;也成为开发者学习和成长的宝藏&#xff0c;但同时也因为其规模庞大、代…

STM32-CAN总线

1.CAN总线简介 CAN总线是由BOSCH公司开发的一种简洁易用、传输速度快、易扩展、可靠性高的串行通信总线 2.CAN总线特征 两根通信线&#xff08;CAN_H、CAN_L&#xff09;&#xff0c;线路少&#xff0c;无需共地差分信号通信&#xff08;相对的是单端信号&#xff09;&#…

机器学习-线性回归(简单回归、多元回归)

这一篇文章&#xff0c;我们主要来理解一下&#xff0c;什么是线性回归中的简单回归和多元回归&#xff0c;顺便掌握一下特征向量的概念。 一、简单回归 简单回归是线性回归的一种最基本形式&#xff0c;它用于研究**一个自变量&#xff08;输入&#xff09;与一个因变量&…

Linux 高级路由与流量控制-用 tc qdisc 管理 Linux 网络带宽

大家读完记得觉得有帮助记得关注和点赞&#xff01;&#xff01;&#xff01; 此分享内容比较专业&#xff0c;很多与硬件和通讯规则及队列&#xff0c;比较底层需要有技术功底人员深入解读。 Linux 的带宽管理能力 足以媲美许多高端、专用的带宽管理系统。 1 队列&#xff0…

安装k8s前置操作(Ubuntu / CentOS)

本文将介绍在 Ubuntu 和 CentOS 系统上进行 Kubernetes 部署前的基本配置步骤。不同的操作系统有不同的配置方法&#xff0c;但大部分操作是相似的。本文章将分为两个部分&#xff1a;第一部分是 Ubuntu 前置操作&#xff0c;第二部分是 CentOS 前置操作。 第一节&#xff1a;U…

Flask简介与安装以及实现一个糕点店的简单流程

目录 1. Flask简介 1.1 Flask的核心特点 1.2 Flask的基本结构 1.3 Flask的常见用法 1.3.1 创建Flask应用 1.3.2 路由和视图函数 1.3.3 动态URL参数 1.3.4 使用模板 1.4 Flask的优点 1.5 总结 2. Flask 环境创建 2.1 创建虚拟环境 2.2 激活虚拟环境 1.3 安装Flask…

C语言教程——动态内存管理(2)

文章目录 前言一、pandas是什么&#xff1f;二、使用步骤 1.引入库2.读入数据 总结 前言 我们之前学了动态内存管理分配函数&#xff0c;也是熟悉了动态内存分配函数&#xff0c;基于动态内存分配我把之前的通讯录做了修改&#xff0c;上传到了gitee上&#xff0c;这篇文章接着…

python学习笔记3-字符串常用的方法

一、判断&#xff08;9个&#xff09;&#xff1a; 二、查找和替换&#xff08;8个&#xff09; 三、⼤⼩写转换&#xff08;5个&#xff09; 四、⽂本对⻬&#xff08;3个&#xff09; 五、去除空⽩字符&#xff08;3个&#xff09; 六、拆分和连接 &#xff08;6个&#xff0…

MySQL主从配置

一、 主从原理 MySQL 主从同步是一种数据库复制技术&#xff0c;它通过将主服务器上的数据更改复制到一个或多个从服务器&#xff0c;实现数据的自动同步。主从同步的核心原理是将主服务器上的二进制日志复制到从服务器&#xff0c;并在从服务器上执行这些日志中的操作。 二、主…