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

news2024/11/26 10:41:29

文章目录

  • 死锁的出现场景
    • 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/1985771.html

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

相关文章

【iOS】暑假第二周——网易云APP 仿写

目录 前言首页关于UINavigationBarAppearance “我的”账号夜间模式——多界面传值遇到的问题所用到的其他知识整理NSNotificationreloadData各种键盘模式 总结 前言 有了之前仿写ZARA的基础,本周我们仿写了网易云APP,在这里对多界面传值进行了首次应用—…

LISA: Reasoning Segmentation via Large Language Model

发表时间:CVPR 2024 论文链接:https://openaccess.thecvf.com/content/CVPR2024/papers/Lai_LISA_Reasoning_Segmentation_via_Large_Language_Model_CVPR_2024_paper.pdf 作者单位:CUHK Motivation:尽管感知系统近年来取得了显…

基于SSH的医院在线挂号系统设计与实现

点击下载源码 基于SSH的医院在线挂号系统设计与实现 摘 要 互联网技术迅速的发展给我们的生活带来很大的方便,同时也让许多行业迅速的发展起来。互联网技术已走向科技发展的巅峰期,我们要做的就是合理的使用互联网技术让我们的各个行业得到更快速的发展…

2024杭电多校06——1005交通管控

补题点这里 大意 一个操作杆可以对k个红绿灯进行操作,操作杆上的一个字符对应一个红绿灯,操作包括,-,0,问每种组合方案有多少种组合方式 : red->green->yellow->red -:green->red->yellow->green 可以用一个三进制数表示每个灯的状态…

Python(模块---pandas+matplotlib+pyecharts)

import pandas as pd import matplotlib.pyplot as plt dfpd.read_excel(简易数据.xlsx) # print(df) plt.rcParams[font.sans-serif][SimHei] #设置画布的大小 plt.figure(figsize(10,6)) labelsdf[电影中文名] ydf[国籍] # print(labels) # print(y)# import pandas as pd im…

[Webpack]webpack-dev-server设置多个路径代理时,proxy顺序有要求

问题背景 前端需要调用多个不同的后台时需要使用devServer.proxy做代理 问题现象 如下图设置ETL相关接口路径代理之后 调用ETL后台接口时产生404报错 问题原因 devServer.proxy在解析代理路径并替换的时候是按顺序解析的,我配置的三个代理中,/csm…

NCL数据分析与处理实践技术

NCAR Command Language(NCL)是由美国大气研究中心(NCAR)推出的一款用于科学数据计算和可视化的免费软件。它有着非常强大的文件输入和输出功能,可读写netCDF-3、netCDF-4 classic、HDF4、binary、ASCII数据&#xff0c…

Linux之软硬链接和动静态库

个人主页:点我进入主页 专栏分类:C语言初阶 C语言进阶 数据结构初阶 Linux C初阶 算法 C进阶 欢迎大家点赞,评论,收藏。 一起努力,一起奔赴大厂 目录 一.软硬链接 1.1如何软硬链接 1.2软硬链接的作用 …

ViP-LLaVA: Making Large Multimodal Models Understand Arbitrary Visual Prompts

发表时间:cvpr2024 论文链接:https://readpaper.com/pdf-annotate/note?pdfId2357936887983293952&noteId2426262228488986112 作者单位:University of Wisconsin–Madison Motivation:现在的多模态模型都关注整张图像的理…

html+css网页设计 qq官网首页1个页面无js

htmlcss网页设计 qq官网首页1个页面无js功能 页面1:1还原 网页作品代码简单,可使用任意HTML编辑软件(如:Dreamweaver、HBuilder、Vscode 、Sublime 、Webstorm、Text 、Notepad 等任意html编辑软件进行运行及修改编辑等操作)。 …

冲击性信号的频域特征

这是一个信号采样数学实验,你可以直观感受到冲击信号的时域和频域特征 1.原始冲击信号: 原始信号是一个频率为180Hz附近的一个冲击性信号: 2.冲击信号频谱 它的频谱,可能会超出你的想象,它的1x频率幅度可能并不最高…

iOS ------ autoreleasePool

一,autoReleasePool{} int main(int argc, const char * argv[]) {autoreleasepool {}return 0; }我们平时创建一个main函数的代码的时候,就会发现其中有一个这个东西autoreleasepool{},使用clang编译之后:autoreleasepool{…}被…

对《国家汽车芯片标准体系建设指南》好奇,遂读

基础通用:基于汽车行业对芯片的可靠性、运行稳定性 和安全性等应用需求,提取出汽车芯片性通用要求,主要包括环境及可靠性、电磁兼容、功能安全和信息安全共4个方面的要求。 产品与技术应用:根据实现功能的不同,将汽车…

文献综述能否帮助研究人员认识特定学术领域的趋势和新兴主题

VersaBot一键生成文献综述 进行良好的文献综述可以成为研究人员识别特定学术领域的趋势和新兴主题的强大工具。就是这样; 1. 识别模式和重复出现的概念: 当您深入研究现有研究时,您自然会开始注意到不同研究中采用的重复出现的主题、想法和方法。这些模…

详解爬虫使用代理ip的几种方案

​ 在如今这个信息爆炸的时代,数据就是财富。对于许多从事数据分析、市场调研和大数据处理的人来说,网络爬虫已经成为了他们的得力助手。然而,随着网站对爬虫的防范措施越来越严格,使用代理IP已经成为了爬虫工作中的一项必备技能。…

05:【stm32】重映射AFIO

重映射AFIO 1、什么是AFIO2、怎么使用AFIO 1、什么是AFIO AFIO是stm32上的众多片上外设之一,专门用来执行“复用功能的重映射” 2、怎么使用AFIO 如下图所示:当我们要同时使用USART1和TIM1时,我们就需要使用AFIO进行使其中一个片上外设进行重…

【iOS】SideTable

目录 SideTablesStripedMapSideTable1. spinlock_t slock2. RefcountMap3. weak_table_t 总结 objc4源码地址: SideTable& table SideTables()[this]; // 获取对象的SideTable size_t& refcntStorage table.refcnts[this];SideTables 查源码SideTables…

Android 多语言切换

文章目录 在系统设置修改语言创建资源目录创建资源文件示例验证 代码手动切换语言在Application中设置新的语言环境在MainActivity / BaseActivity中设置新的语言环境验证 问题1. makeText()方法context传入是Application的context,无法获取正确的资源字符串原因解决…

Docker + Nacos + Spring Cloud Gateway 实现简单的动态路由配置修改和动态路由发现

1.环境准备 1.1 拉取Nacos Docker镜像 从Docker Hub拉取Nacos镜像: docker pull nacos/nacos-server:v2.4.01.2 生成密钥 你可以使用命令行工具生成一个不少于32位的密钥。以下是使用 OpenSSL 生成 32 字节密钥的示例: openssl rand -base64 321.3 …

免费插件集-illustrator插件-Ai插件-选择路径等分

文章目录 1.介绍2.安装3.通过窗口>扩展>知了插件4.功能解释5.总结 1.介绍 本文介绍一款免费插件,加强illustrator使用人员工作效率,路径处理功能,功能是选择路径等分。首先从下载网址下载这款插件 https://download.csdn.net/download…