Java: 死锁问题详解(5000字)

news2024/11/16 11:29:06

文章目录

  • 死锁的出现场景
    • 1. 一个线程一把锁,这个线程针对这把锁,连续加锁了两次
    • 2. 两个线程,两把锁
    • 3. N个线程 , M个锁
    • 4. 内存可见性
      • 为什么会出现内存可见性问题呢?
      • 解决方法 volatile关键字
  • 总结
    • synchronized:
    • 死锁的四个必要条件(缺一不可)[重点]:
    • 内存可见性问题:


死锁的出现场景

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

死锁的场景1:

void func() {
	//第一次能够加锁成功
	synchronized (this) {
		//第二次加锁的时候,锁对象已经被占用了
		//第二次加锁就应该阻塞
		synchronized (this) {
		
		}
	}
}

这个情况在代码实例中,并没有出现死锁,这是因为synchronized针对这个情况做了特殊处理~

C++ / Python中的锁就没有这样的功能,就会死锁(借助第三方库可以实现不出现死锁)

synchronized 是 “可重入锁”, 针对上述一个线程连续加锁两次的情况,synchronized 在加锁的时候,不仅需要判定当前的锁是否是被占用的状态,还要在锁中额外记录一下当前是哪个线程对这个锁加锁了~
对于可重入锁来说,发现加锁的线程就是当前锁的持有线程,并不会真正进行任何的加锁操作,也不会进行任何的"阻塞操作",而是直接往下执行.
在这里插入图片描述

那么问题就来了,计算机是怎么知道哪一个是需要真正释放锁的操作呢,换句话说,计算机是怎么知道哪一个是最外层的括号呢 ?

  • 针对上述问题,我们可以引入一个计数器~
    初始情况下,计数器是0
    每次执行到 { 计数器 +1
    每次执行到 } 计数器 -1
    如果某次 -1 后,计数器为0了,那么就说明这次就要真正的释放锁了~
    在这里插入图片描述

这是计算机中非常常见的思想方法,它在JVM中的垃圾回收机制,C++智能指针,Linux等等都用到了.

2. 两个线程,两把锁

死锁的场景2:

  1. 首先线程1 现针对 A 加锁,线程2 针对 B 加锁
  2. 之后线程1 不释放锁A 的情况下,再针对 B 加锁.同时线程 2 不释放 B 的情况下针对 A 加锁

也就是说出现了"循环依赖".

举个例子:
程序员来到公司楼下,被保安拦住了.
保安: 请出示一码通.
程序员: 我得上楼,修了bug,才能出示一码通.
保安: 你得出示一码通,才能上楼.┗( ▔, ▔ )┛

写成代码:

public class Demo12 {
    private static String lock1 = "";//锁1
    private static String lock2 = "1";//锁2

    public static void main(String[] args) throws InterruptedException {
    	//线程1
        Thread t1 = new Thread(()->{
            synchronized(lock1) {
                System.out.println("t1 lock1");

				//这里的sleep是为了确保t1和t2都分别拿到lock1和lock2,然后再分别拿对方的锁
				//如果没有sleep,那么执行顺序就不可控,可能会出现某个线程一口气拿到两把锁,另一个线程还没执行呢,无法构造出死锁
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized(lock2) {
                    System.out.println("t1 lock2");
                    //没有打印出来.说明被线程1被阻塞了
                }
            }
        });

		//线程2
        Thread t2 = new Thread(()->{
            synchronized(lock2) {
                System.out.println("t2 lock2");
                try {
                    Thread.sleep(1000);
                } catch(InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized(lock1) {
                    System.out.println("t2 lock1");
                    //没有打印出来.说明被线程2被阻塞了
                }
            }
        });

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

在这里插入图片描述

3. N个线程 , M个锁

有一个经典模型: 哲学家就餐问题

有5个哲学家坐在一块吃面条,任意一个哲学家想要吃到面条都需要拿起左手和右手的筷子~
这5个哲学家会做两件事:

  1. 思考人生,放下手里的筷子
  2. 吃面条,拿起左右手两边

在这里插入图片描述

通常情况下,这个模型是可以运转的,但是一旦出现极端情况,就会死锁.
比如,每个哲学家同时拿左手边的筷子,此时每个筷子都被拿起来了,哲学家的右手就拿不起筷子了(因为桌子上没有了),由于哲学家非常固执,当他吃不到面条的时候,也绝对不会放下左手的筷子.
于是谁都吃不到面条(哲学家: 没错我就是这么固执 o(´^`)o).

想一想该如何解决上述问题呢?
很简单,给每个筷子编个号(1,2,3,…,N),然后让所有的哲学家先拿起编号小的筷子,后拿起编号大的筷子.
在这里插入图片描述
只要遵守上述的拿起筷子的顺序,无论接下来这个模型的运行顺序如何,无论出现多么极端的情况,都不会再死锁了.

把哲学家看做线程,把筷子看做锁,这就是死锁的第三种情况了~

4. 内存可见性

内存可见性问题是指: 当一个线程对共享变量进行了修改后,其他线程可能无法立即看到这个修改。

这么说可能有点抽象,举个栗子:

import java.util.Scanner;

public class Demo13 {
    private static int n = 0;
    public static void main(String[] args) throws InterruptedException{
        Thread t1 = new Thread(()->{
            while (n == 0) {
                //啥都不写
            }
            System.out.println("t1线程结束");
        });
        Thread t2 = new Thread(()->{
            Scanner scanner = new Scanner(System.in);
            System.out.print("输入n的值: ");
            n = scanner.nextInt();
           System.out.println("t2线程结束");
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("主线程结束");
    }
}

运行结果:
在这里插入图片描述
这这这,这不对吧,我们不是已经输入非0的值了吗,n应该不是0了呀,线程t1中的循环的条件不成立了,t1应该结束啊.
但是实际上,我们输入10后,t1没有任何动静!!
通过jconsole看到t1线程(Thread-0)仍然是持续工作的~
在这里插入图片描述
出现上述问题的原因,就是"内存可见性问题"

为什么会出现内存可见性问题呢?

Thread t1 = new Thread(()->{
            while (n == 0) {
                //啥都不写
            }
            System.out.println("t1线程结束");
        });

在t1线程中的循环会执行非常多次, 每次循环都需要执行n == 0 这样的判定,

  1. 从内存读取数据到寄存器中(读取内存,相比之下,这个操作的非常慢)
  2. 通过类似于cmp指令,比较寄存器和0的值(这个指令的执行速度非常快)

对于计算机来说,存储数据的设备,有一下几个层次

  1. CPU寄存器: 空间小,速度快,成本高,数据掉电后丢失
  2. 内存: 空间中等,速度中等,数据掉电后丢失
  3. 硬盘: 空间大,速度慢,成本低,数据掉电后不丢失

每一个层次之间大约相差3~4个数量级

此时JVM执行这个代码的时候,发现:
每次循环的过程中,执行"读取内存"这个操作,开销非常大.
而且每次执行"读取内存"这个操作,结果都是一样的呀.
并且JVM根本没有意识到,用户可能在未来会修改n
于是JVM就做了个大胆的操作—直接把"读取内存"这个操作给优化掉了.

每次循环,不会重新读取内存中的数据,而是直接读取寄存器/cache中的数据(缓存中的结果)

当JVM做出上述决定之后,此时意味着循环的开销大幅度降低了~
但是当用户修改n的时候,内存中的n已经改变了
但是由于t1线程每次循环,不会真的读内存,于是就感知不到n的改变

这样就引起了bug — “内存可见性问题”

内存可见性问题,本质上,是编译器/JVM 对代码进行优化的时候,优化出bug了
如果代码是单线程的,编译器/JVM对代码的优化一般是非常准确的,优化之后,不会影响到逻辑.

但是代码如果是多线程的,编译器/JVM 的代码优化,就有可能出现误判(编译器 / JVM的bug)
导致不该优化的地方,也给优化了,于是就造成了内存可见性问题.

说点题外话:

编译器为啥要做上述的代码优化,为啥不老老实实地按照程序员写的代码,一板一眼的执行呢?

主要是因为,有的程序员,写出来的代码,太低效了.
为了能够降低程序员的门槛,即使你代码写的一般,最终的执行速度也不会落下风.
因此,主流编译器,都会引入优化机制.
也就是说,编译器会自动调整你的代码,使其在保持原有逻辑不变的情况下,提高代码的执行效率.

编译器优化,本身也是一个复杂的话题
某个代码,何时优化,优化到啥程度,都不好说~
开放编译器的大佬们,有一系列的策略来实现这里的优化功能.
咱们站在外行人的角度,是很难判断某个代码是否优化的. 代码稍微改变一点,优化结果就会截然不同~

解决方法 volatile关键字

如果我们希望代码正常运行,该咋办呢[・ヘ・?]

说白了,之所以会出现"内存可见性问题",这不就是因为编译器优化出bug了吗,我们告诉编译器:“誒,你别优化这里~”.不就可以啦!
锵锵锵锵,"volatile"关键字就可以做到上述操作!

volatile关键字: 修饰一个变量,提示编译器说,这个变量是"易变"的.
编译器进行上述优化的前提,是编译器认为针对这个变量的频繁读取,结果都是固定的.

对变量加上volatile关键字后,编译器就会禁止上述的优化,从而确保每次循环都从内存中重新读取数据~

对volatile关键字更进一步的理解:
在引入volatile关键字后,编译器在生成这个代码的时候,就会在这个变量的读取操作附近生成一些特殊的指令,称为"内存屏障".
后续JVM执行到这些特殊指令,就知道了,不能进行上述优化了~

总结

synchronized:

  • 是可重入锁
  • 可重入锁内部记录了当前是哪个线程持有的锁,后续加锁的时候都会进行判定~
  • 它还会通过一个引用计数,来维护当前的加锁次数,从而描述出何时真正释放锁.

死锁的四个必要条件(缺一不可)[重点]:

  1. 锁是互斥的[锁的基本特性]
  2. 锁是不可抢占的,线程1拿到了锁A,如果线程1不主动释放A,线程2是不能把锁A抢过来的 [锁的基本特性]
  3. 请求和保持.线程1拿到锁之后,不释放A的前提下,去拿锁B [代码结构](我们在写代码时要避免出现锁的嵌套.)
  4. 循环等待 / 环路等待 / 循环依赖. 多个线程获取锁的过程,存在 循环等待~[代码结构]

假设代码按照请求和保持的方式,获取到N个锁,那么该如何避免出现循环等待呢?
一个简单有效的办法: 给锁编号,并约定所有的线程在加锁的时候都必须按照一定的顺序来加锁.

内存可见性问题:

内存可见性问题是指: 当一个线程对共享变量进行了修改后,其他线程可能无法立即看到这个修改。

出现内存可见性问题的原因是编译器会对代码进行优化,结果给整出bug了.

volatile 关键字:修饰某个指定的变量,告诉编译器,这个变量的值是"易变"的,编译器看到这个标志,就不会把读取内存操作,优化成读取寄存器 / cache.也就是说,volatile可以保持指定的变量,对应的内存,总是可见的~


本文到这里就结束了~
╰( ´・ω・)つ──☆✿✿✿

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

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

相关文章

PCL安装与配置(PCL1.9.1+MSVC2017)

为了和我的VS的版本VS 2017对应,PCL下载的也是msvc_2017,PCL msvc2017最新的则是1.901版本,我们就以PCL 1.9.1为例了。(如果你的vs是2019和2022,一定要注意PCL的版本)。 一、下载PCL 我们打开PCL的github下载地址&am…

GDB调试器

GDB调试器 GDB的主要功能 常见命令 3、实战 1、生成能调试的执行文件(一定要加-g) 第一个是不能调试的 第二个这样加了-g才能进行调试 如果没加-g 执行gdb 执行文件(会报下面这个 ) 像这样才是正常的 执行 gdb a_yes_g 这…

SSM计算机组成原理课程平台-计算机毕设定制-附项目源码(可白嫖)50168

摘 要 21世纪的今天,随着社会的不断发展与进步,人们对于信息科学化的认识,已由低层次向高层次发展,由原来的感性认识向理性认识提高,管理工作的重要性已逐渐被人们所认识,科学化的管理,使信息存…

金融行业到底该选择什么样的FTP替代方案?

2018年以来,受“华为、中兴事件”影响,我国科技尤其是上游核心技术受制于人的现状对我 国经济发展提出了严峻考验。在全球产业从工业经济向数字经济升级的关键时期,中国明确 “数字中国”建设战略, 抢占数字经济产业链制高点。 在…

Python开发工具PyCharm入门指南 - 用户界面主题更改

JetBrains PyCharm是一种Python IDE,其带有一整套可以帮助用户在使用Python语言开发时提高其效率的工具。此外,该IDE提供了一些高级功能,以用于Django框架下的专业Web开发。 界面主题定义了窗口、对话框、按钮和用户界面的所有可视元素的外观…

vscode开发avalonia

安装 安装.net 8 安装avalonia模板 dotnet new install Avalonia.Templates创建项目 dotnet new avalonia.app -o GetStartedApp安装c# dev kit插件和Avalonia for VSCode Community dotnet run运行 修改代码 MainWindow.axaml <Window xmlns"https://githu…

企业层面经济政策不确定性感知数据(2001-2023年)

1.指标介绍 企业经济政策不确定性感知指的是企业在面对政府经济政策变动时所感受到的风险和不确定性程度&#xff0c;这种感知会影响企业的投资决策、生产计划和市场策略 文章根据上市公司披露的MD&A文本&#xff0c;提取指标衡量企业个体面临的经济政策不确定性。 2.参…

了解线性回归、岭回归和套索回归

逐步对 Linear、Ridge 和 Lasso 回归进行数学理解。 ​ LASSO&#xff08;左&#xff09;和岭回归&#xff08;右&#xff09;的约束区域 一、说明 在本文中&#xff0c;我们将深入探讨机器学习中两种基本正则化技术的基础和应用&#xff1a;Ridge 回归和 Lasso 回归。这些方…

脊髓损伤小伙伴的活力重启秘籍! 让我们一起动起来,拥抱不一样的精彩生活✨

Hey小伙伴们~&#x1f44b; 今天咱们来聊聊一个超级重要又温暖的话题——脊髓损伤后的锻炼大法来啦&#xff01;&#x1f389; 记住&#xff0c;无论遇到什么挑战&#xff0c;我们都要像打不死的小强一样&#xff0c;活力满满地面对每一天&#xff01;&#x1f4aa; 首先&#…

基础进阶-搭建pxe网络安装环境实现服务器自动部署

目录 原理解释 ​编辑 开机界面解释 搭建步骤 下载环境需要用到的基本程序 查看帮助 帮助内容解释 环境搭建 修改 DHCP 修改 default 文件 测试 原理解释 开机界面解释 在开机界面中&#xff0c;圈起来的部分显示的就是光盘&#xff0c;我们需要将光盘转换成网络的 在…

.NET内网实战:模拟Installer关闭Defender

01基本介绍 02编码实现 原理上通过Windows API函数将当前进程的权限提升至TrustedInstaller&#xff0c;从而实现了对Windows Defender服务的控制。通常可以利用Windows API中的OpenSCManager、OpenProcessToken、ImpersonateLoggedOnUser以及ControlService等函数协同工作&am…

Modbus-Ascii详解

目录 Modbus-Ascii详解 Modbus-Ascii帧结构 LRC效验 将数据字节转成ASCII 将返回帧转为数据字节 Modbus-Ascii的实现 使用NModbus4实现Modbus-Ascii 实例 Modbus-Ascii详解 Modbus ASCII是一种将二进制数据转换为可打印的ASCII字符的通信协议&#xff0c;‌每个8位数据需要两…

WPF学习(4)- VirtualizingStackPanel (虚拟化元素)+Canvas控件(绝对布局)

VirtualizingStackPanel虚拟化元素 VirtualizingStackPanel 类&#xff08;虚拟化元素&#xff09;和StackPanel 类在用法上几乎差不多。其作用是在水平或垂直的一行中排列并显示内容。它继承于一个叫VirtualizingPanel的抽象类&#xff0c;而这个VirtualizingPanel抽象类继承…

AI基础架构-NVLink 技术详解

AI Infra 基础知识 - NVLink 入门 NVLink&#xff0c;一种专有互连硬件&#xff0c;实现Nvidia GPU与CPU之间的高效、一致数据和控制传输&#xff0c;提升多GPU系统性能。 概述 NVLink 于 2014 年初发布&#xff0c;旨在作为 PCI Express 的替代解决方案&#xff0c;具有更…

Java零基础之多线程篇:线程同步

哈喽&#xff0c;各位小伙伴们&#xff0c;你们好呀&#xff0c;我是喵手。运营社区&#xff1a;C站/掘金/腾讯云&#xff1b;欢迎大家常来逛逛 今天我要给大家分享一些自己日常学习到的一些知识点&#xff0c;并以文字的形式跟大家一起交流&#xff0c;互相学习&#xff0c;一…

Java13.0标准之重要特性及用法实例(二十三)

简介&#xff1a; CSDN博客专家&#xff0c;专注Android/Linux系统&#xff0c;分享多mic语音方案、音视频、编解码等技术&#xff0c;与大家一起成长&#xff01; 新书发布&#xff1a;《Android系统多媒体进阶实战》&#x1f680; 优质专栏&#xff1a; Audio工程师进阶系列…

【第三版 系统集成项目管理工程师】第9章 项目管理概论

持续更新。。。。。。。。。。。。。。。 【第三版】第九章 项目管理概论 9.1 PMBOK的发展9.2 项目基本要素9.2.1项目基础 P3041.独特的产品、服务或成果-P3042.临时性工作-P3043.项目驱动变更-P3054.项目创造业务价值-P3055.项目启动背景-P306 9.2.2项目管理 P3069.2.2 项目管…

AQS的ReentrantLock源码

什么是AQS&#xff08;全称AbstractQueuedSynchronizer&#xff09; 代表&#xff1a;重入锁、独占锁/共享锁、公平锁/非公平锁 是JUC包中线程阻塞、阻塞队列、唤醒、尝试获取锁的一个框架 AbstractQueuedSynchronizer是全称&#xff0c;是一个模板模式&#xff0c;一些线程…

深入理解Java的内存管理机制

文章目录 1. 程序计数器 (Program Counter Register)2. Java虚拟机栈 (Java Virtual Machine Stack)3. 本地方法栈 (Native Method Stack)4. 堆 (Heap)a. 年轻代 (Young Generation)b. 老年代 (Old Generation) 5. 方法区 (Method Area)6. 运行时常量池 (Runtime Constant Pool…

magic-api相关应用与配置

目录 项目启动 工具&#xff1a;IDEA 运行项目 关于配置 项目启动 工具&#xff1a;IDEA 新建——》项目——》导入——》运行 运行项目 http://localhost:9999/magic/web/index.htmlhttp://localhost:9999/magic/web/index.html 关于配置 配置多数据源 在线配置多数据…