做网站龙华/seo网站内部优化
目录
一、死锁
1.1 出现死锁的常见场景:
1.2 产生死锁的后果:
1.3 如何避免死锁:
二、内存可见性
2.1 由内存可见性产生的经典案例:
2.2 volatile 关键字:
2.2.1 volatile 用法:
2.2.2 volatile 不保证原子性:
2.2.3 volatile 作用总结:
三、wait 和 notify
3.1 wait 详解:
3.2 notify 和 notifyAll:
3.2.1 notify:
3.2.2 notifyAll:
3.3 面试题:wait 和 sleep 的区别:
在上一篇文章,我们了解了什么是线程安全,分析了产生线程不安全的原因。今天我们就要深度刨析一下线程不安全的经典案例:死锁和内存可见性引起的线程不安全问题。
一、死锁
1.1 出现死锁的常见场景:
• 场景一:
锁是不可重入锁(synchronized 是可重入锁),并且一个线程针对一个锁对象,连续加锁两次。
• 场景二:
两个线程两把锁。先让两个线程分别拿到一把锁,然后再去尝试获取对方的锁,这时就出现了死锁的情况。
• 场景三:
多个线程,多把锁。随着线程和锁的数目的增加,情况就会变得更加复杂,死锁就更容易出现。下面就是一个经典的死锁场景:哲学家就餐(除非吃到面条,否则不会放下筷子)。
如果出现极端的情况,同一时刻所有的哲学家都拿起左边的筷子,这时就会出现死锁。
1.2 产生死锁的后果:
死锁是非常严重的问题。一个进程中线程的个数是有限的,死锁会使线程被卡住,没法继续工作。更加严重的是,死锁这种 bug 往往都是概率性出现(未知才是最可怕的)。测试的时候,怎么测试都没事,一旦发布,就出现了问题。更加要命的是发布也没有问题,等到夜深人静的时候,大家都睡着的时候,突然给你来点问题,直接带走年终奖😭。
1.3 如何避免死锁:
要想避免死锁,我们就要从产生死锁的原因入手。
教科书上经典的产生死锁的四个必要条件(下面给出的四个条件,友友们一定要背下来,面试的经典问题)。
1. 锁具有互斥性:
这时锁的基本特点,一个线程拿到锁之后,其他线程就得阻塞等待。
2. 锁具有不可抢占性(不可剥夺性):
一个线程拿到锁之后,除非他自己主动释放锁,否则谁也抢不走。
3. 请求和保持:
一个线程拿到一把锁之后,不释放这个锁的前提下,再尝试获取其他锁。
4. 循环等待。
多个线程获取多个锁的过程中,出现了循环等待,A 等待 B ,B 又等待 A。
在任何一个死锁的场景,都必须同时具备上述四点,只要缺少一个,都不会构成死锁。观察上面的四个条件不难发现条件 1 和条件 2 是锁的基本特性,这个我们无法改变,观察到条件 3 和条件 4 都是代码结构的问题,所以我们就从条件 3,4 入手。
• 针对条件 3:
不要让锁嵌套获取即可。如果有些场景必须要嵌套获取锁,那么就破除循环等待(条件 4 ),即使出现嵌套,也不会出现死锁。
• 针对条件 4:
当代码中,确实需要用到多个线程获取多把锁,一定要记得约定好加锁的顺序(每个线程都必须要先获取 A 锁,再获取 B 锁,再.......),就可以有效避免死锁了。
二、内存可见性
2.1 由内存可见性产生的经典案例:
请友友们观察一下下面这段代码,可以粘贴到自己的编译器上跑一下,看看是否符合你的预期。
public class demo1 {static int count = 0;public static void main(String[] args) {Thread t1 = new Thread(() -> {while(count == 0){}System.out.println("t1.end");});Thread t2 = new Thread(() -> {Scanner in = new Scanner(System.in);System.out.println("请输入一个数字:");count = in.nextInt();});t1.start();t2.start();}
}
因为输入数据存在 IO 操作(很慢)所以一定能保证在我们输入数据的时候,t1 线程已经开始执行了。
正常来说,我们输入一个非 0 的数字后,t1 线程里面就会停止循环。但是产生的结果如下:
循环并没有退出,由于是前台线程,所以程序不能够结束。
上述问题产生的原因就是因为内存可见性。
• 案例解析:
上面的案例产生的问题是由于编译器优化 / JVM 优化产生的问题。不是说优化不好,而是 JVM 在这种情况下的优化太激进了。为什么会产生这么激进的优化呢?
我们站在指令的角度来理解有两个方面:
1. 在while 循环体中,每次条件判断的时候,分为两个步骤:1. load:从内存读取数据到 cpu 寄存器。2. cmp:比较,条件成立就会继续执行。 当前循环的旋转速度很快,短时间内出现大量的 load 和 cmp 反复执行的效果,由于 load 执行消耗的时间比 cmp 消耗的时间多很多(量级是几千倍,上万倍)。
2. JVM 发现每次 load 执行的结果是一样的(在 t2 修改之前)。
于是 JVM 就把上述的 load 操作优化掉了,只有第一次是真正的进行 load 后续的 load 就直接读取刚才 load 在寄存器中的值,也就是说不会去内存中去读取值了,这时即使内存中的值已经修改,但是还是 load 不到,这就是我们的线程可见性问题。
其实在这里加上打印,程序就符合我们的预期了。
这是为什么呢?答:因为此时 IO 操作才是程序运行时间的大头,优化 load 就没有必要了,因为程序的瓶颈不是 load 。此外,IO 操作是不能被优化掉的,被优化的前提是反复执行的结果是相同的,IO 操作注定是反复执行的结果是不相同的。
• 小结:
上述问题的本质还是编译器优化引起的,优化掉 load 操作之后,使 t2 线程的修改,没有被 t1 线程感知到,这就是 ”内存可见性“ 问题。
2.2 volatile 关键字:
2.2.1 volatile 用法:
编译器到底啥时候优化这也是个 ”玄学问题“。我们作为程序员显然不希望看到这样的代码出现,因此 Java 就引入的 volatile 关键字,就可以解决内存可见性引起的问题。volatile 修饰的变量,能够保证 "内存可见性"。
代码在写入 volatile 修饰的变量的时候:
• 改变线程工作内存中 volatile 变量副本的值。
• 将改变后的副本的值从工作内存刷新到主内存。
代码在读取 volatile 修饰的变量的时候:
• 从主内存中读取 volatile 变量的最新值到线程的工作内存中。
• 从工作内存中读取 volatile 变量的副本。
前面我们讨论内存可见性时说了,直接访问工作内存(实际是 CPU 的寄存器或者 CPU 的缓存),速度非常快,但是可能出现数据不一致的情况。加上 volatile ,强制读写内存。速度是慢了,但是数据变的更准确了。
使用案例演示:
public class demo2 {public volatile static int count = 0;public static void main(String[] args) {Thread t1 = new Thread(() -> {System.out.println("t start");while(count == 0){}System.out.println("t end");});Thread t2 = new Thread(() -> {Scanner in = new Scanner(System.in);System.out.println("请输入一个数字:");count = in.nextInt();});t1.start();t2.start();}
}
案例效果:
2.2.2 volatile 不保证原子性:
volatile 和 synchronized 有着本质的区别。synchronized 能够保证原子性,volatile 保证的是内存可见性。例如下面这个案例:
public class demo3 {public volatile static int count = 0;public static void main(String[] args) {Object locker = new Object();Thread t1 = new Thread(() -> {for(int i = 0;i < 50000;i++){
// synchronized(locker){count++;
// }}});Thread t2 = new Thread(() -> {for(int i = 0;i < 50000;i++){
// synchronized(locker){count++;
// }}});t1.start();t2.start();try {t1.join();} catch (InterruptedException e) {throw new RuntimeException(e);}try {t2.join();} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println(count);}
}
案例演示效果如下:
可以看到最终的结果还是不符合我们的预期,所以 volatile 不保证原子性。
2.2.3 volatile 作用总结:
volatile 关键字的作用主要有如下两个:
• 保证内存可见性:
基于屏障指令实现,即当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。
• 保证有序性:
禁止指令重排序。编译时 JVM 编译器遵循内存屏障的约束,运行时靠屏障指令组织指令顺序。
注意:volatile 不能保证原子性。
三、wait 和 notify
由于线程之间是抢占式执行的,因此线程之间执行的先后顺序难以预知。但是实际开发中有时候我们希望合理的协调多个线程之间的执行先后顺序。就好像足球队一样,线程 1 要先传球,线程 2 才能射门。
针对随即调度,我们程序员也是有手段干预的,即通过 “等待” 的方式,能够让线程一定程度的按照我们预期的顺序来执行。无法主动让某个线程被调度,但是可以主动让某个线程等待(给别的线程机会)。
完成这个协调工作,主要涉及到三个方法:
注意: wait,notify,notifyAll 都是 Object 类的方法(意味着所有类都可以)。
3.1 wait 详解:
wait 做的事:
• 使当前执行代码的线程进行等待(把线程放到等待队列中)。
• 释放当前的锁(wait 必须要放在锁的代码块里面使用)。
• 满足一定条件时被唤醒,重新尝试获取这个锁。
wait 结束等待的条件:
• 其他线程调用该对象的 notify 方法。
• wait 等待时间超时(wait 方法提供一个带有 timeout 参数的版本,来指定等待时间)。
• 其他线程调用该等待线程的 interrupted 方法,导致 wait 抛出 InterruptedException 异常。
注意:wait 和 sleep 一样会被线程的 interrupt 打断,wait 也会自动清空标志位。
案例演示如下:
public class demo1 {public static void main(String[] args) {Object locker = new Object();Thread t1 = new Thread(() -> {System.out.println("开始");try {synchronized(locker){locker.wait();}} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("结束");});t1.start();}
}
案例演示效果如下:
可以发现线程成功被停止了。
注意:
wait 要搭配 synchronized 来使用。脱离 synchronized 使用 wait 会直接抛出异常。例如我们将上面的那段代码进行修改,将 locker 脱离锁,产生的情况如下:
这样在执行到 object.wait() 之后就一直等待下去,那么程序肯定不能⼀直这么等待下去了。这个时候就需要使用到了另外⼀个方法唤醒的方法 notify()。
3.2 notify 和 notifyAll:
3.2.1 notify:
notify 方法是唤醒等待的线程。具体作用如下:
• 方法 notify() 也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象的对象锁的其它线程,对其发出通知 notify,并使它们重新获取该对象的对象锁。
• 如果有多个线程等待,则有线程调度器随机挑选出⼀个呈 wait 状态的线程。(并没有 "先来后到") 。
• 在 notify() 方法后,当前线程不会马上释放该对象锁,要等到执行 notify() 方法的线程将程序执行完,也就是退出同步代码块之后才会释放对象锁。
案例演示如下:
public class demo2 {public static void main(String[] args) {Object locker = new Object();Thread t1 = new Thread(() -> {System.out.println("开始等待");synchronized(locker){try {locker.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}}System.out.println("结束等待");});Thread t2 = new Thread(() -> {Scanner in = new Scanner(System.in);System.out.print("输入内容开始通知:");in.next();synchronized(locker){locker.notify();System.out.println("通知结束");}});t1.start();t2.start();}
}
案例演示效果如下:
3.2.2 notifyAll:
notify 只能随机唤醒一个由 wait 导致的等待线程,例如:
import java.util.*;
public class demo3 {public static void main(String[] args) {Object locker = new Object();Thread t1 = new Thread(() -> {System.out.println("t1:开始等待");synchronized(locker){try {locker.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}}System.out.println("t1:结束等待");});Thread t2 = new Thread(() -> {System.out.println("t2:开始等待");synchronized(locker){try {locker.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}}System.out.println("t2:结束等待");});Thread t3 = new Thread(() -> {Scanner in = new Scanner(System.in);System.out.print("输入内容开始通知:");in.next();synchronized(locker){locker.notify();System.out.println("通知结束");}});t1.start();t2.start();t3.start();}
}
最终跑出的结果如下:
可以清楚的看到只有一个线程被唤醒了。
因此 Java 引入 notifyAll 来一次性唤醒全部。我们就直接将上述代码稍加修改即可。
import java.util.*;
public class demo3 {public static void main(String[] args) {Object locker = new Object();Thread t1 = new Thread(() -> {System.out.println("t1:开始等待");synchronized(locker){try {locker.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}}System.out.println("t1:结束等待");});Thread t2 = new Thread(() -> {System.out.println("t2:开始等待");synchronized(locker){try {locker.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}}System.out.println("t2:结束等待");});Thread t3 = new Thread(() -> {Scanner in = new Scanner(System.in);System.out.print("输入内容开始通知:");in.next();synchronized(locker){locker.notifyAll();//修改处System.out.println("通知结束");}});t1.start();t2.start();t3.start();}
}
案例的演示效果如下:
可以看到我们所有等待的线程都被唤醒了。
注意:虽然是同时唤醒 2 个线程,但是这 2 个线程需要竞争锁,所以并不是同时执行,而仍然是有先有后的执行。
3.3 面试题:wait 和 sleep 的区别:
• wait:用于线程之间的通信。
• sleep:让线程阻塞一段时间。
相同点:是都可以让线程放弃执行一段时间。
大体的区别分为如下 3 点:
(1)wait 需要搭配 synchronized 使用,而 sleep 不需要。
(2)wait 是 Object 的方法,sleep 是 Thread 的静态方法。
(3)(从状态来) wait 被调用后,当前线程进入 waiting 状态并释放锁,并可以通过 notify 和 notifyAll 方法进行唤醒。sleep 被调用后当前线程进入 TIMED_WAITING 状态,不涉及锁相关的操作。
结语:
其实写博客不仅仅是为了教大家,同时这也有利于我巩固知识点,和做一个学习的总结,由于作者水平有限,对文章有任何问题还请指出,非常感谢。如果大家有所收获的话还请不要吝啬你们的点赞收藏和关注,这可以激励我写出更加优秀的文章。
相关文章:

【JavaEE精炼宝库】多线程(4)深度理解死锁、内存可见性、volatile关键字、wait、notify
目录 一、死锁 1.1 出现死锁的常见场景: 1.2 产生死锁的后果: 1.3 如何避免死锁: 二、内存可见性 2.1 由内存可见性产生的经典案例: 2.2 volatile 关键字: 2.2.1 volatile 用法: 2.2.2 volatile 不…...

使用Ollama+OpenWebUI部署和使用Phi-3微软AI大模型完整指南
🏡作者主页: 点击! 🤖AI大模型部署与应用专栏:点击! ⏰️创作时间:2024年6月6日23点50分 🀄️文章质量:96分 欢迎来到Phi-3模型的奇妙世界!Phi-3是由微软…...

k8s的ci/cd实践之旅
书接上回k8s集群搭建完毕,来使用它强大的扩缩容能力帮我们进行应用的持续集成和持续部署,整体的机器规划如下: 1.192.168.8.156 搭建gitlab私服 docker pull gitlab/gitlab-ce:latest docker run --detach --hostname 192.168.8.156 --publ…...

笔记96:前馈控制 + 航向误差
1. 回顾 对于一个 系统而言,结构可以画作: 如果采用 这样的控制策略,结构可以画作:(这就是LQR控制) 使用LQR控制器,可以通过公式 和 构建一个完美的负反馈系统; a a 但是有上…...

延时任务工具类
自定义工具类 package com.sxfoundation.task;import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.task.TaskRejectedException; import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; import org.spri…...

springboot下载grpc编译文件,报错缺少protoc-gen-grpc-java:1.34.1:exe不存在
报错如图所示 [ERROR] Then, install it using the command: [ERROR] mvn install:install-file -DgroupIdio.grpc -DartifactIdprotoc-gen-grpc-java -Dversion1.34.1 -Dclassifierwindows-x86_64 -Dpackagingexe -Dfile/path/to/file [ERROR] [ERROR] Alternatively, if yo…...

【面试干货】 非关系型数据库(NoSQL)与 关系型数据库(RDBMS)的比较
【面试干货】 非关系型数据库(NoSQL)与 关系型数据库(RDBMS)的比较 一、引言二、非关系型数据库(NoSQL)2.1 优势 三、关系型数据库(RDBMS)3.1 优势 四、结论 💖The Begin…...

JAVA学习-练习试用Java实现“简化路径”
问题: 给定一个字符串 path ,表示指向某一文件或目录的 Unix 风格 绝对路径 (以 / 开头),请将其转化为更加简洁的规范路径。 在 Unix 风格的文件系统中,一个点(.)表示当前目录本身…...

STM32——ADC篇(ADC的使用)
一、ADC的介绍 1.1什么是ADC ADC(Analogto-Digital Converter)模拟数字转换器,是将模拟信号转换成数字信号的一种外设。比如某一个电阻两端的是一个模拟信号,单片机无法直接采集,此时需要ADC先将短租两端的电…...

(文章复现)基于主从博弈的售电商多元零售套餐设计与多级市场购电策略
参考文献: [1]潘虹锦,高红均,杨艳红,等.基于主从博弈的售电商多元零售套餐设计与多级市场购电策略[J].中国电机工程学报,2022,42(13):4785-4800. 1.摘要 随着电力市场改革的发展,如何制定吸引用户选择的多类型零售套餐成为提升售电商利润的研究重点。为…...

深度评价GPT-4o:探索人工智能的新里程碑
在人工智能领域,OpenAI的GPT系列自推出以来就备受瞩目。GPT-4o作为该系列的最新版本,无疑是迄今为止最为强大的一代。它不仅在技术性能上有了质的飞跃,而且在应用的广泛性和深度上都展现出了惊人的潜力。本文将从版本对比、技术能力、使用体验…...

Linux命令篇(六):vi/vim专项
💝💝💝首先,欢迎各位来到我的博客,很高兴能够在这里和您见面!希望您在这里不仅可以有所收获,同时也能感受到一份轻松欢乐的氛围,祝您生活愉快! 文章目录 一、什么是vim二…...

Java 还能不能继续搞了?
金三银四招聘季已落幕,虽说行情不是很乐观,但真正的强者从不抱怨。 在此期间,我收到众多小伙伴的宝贵反馈,整理出132道面试题,从基础到高级,有八股文,也有对某个知识点的深度解析。包括以下几部…...

【日记】遇到了一个很奇怪的大爷(845 字)
正文 花了昨天和今天两天时间,把数据转移完了。这块 2T 的硬盘可以光荣退休了。目前是没什么存储焦虑了。 农发行净开发一些垃圾系统。今天没什么业务,但跟 ActiveX 斗智斗勇了一整天,最后实在搞不过 IE 浏览器。我也懒得管了,又不…...

Python 机器学习 基础 之 处理文本数据 【处理文本数据/用字符串表示数据类型/将文本数据表示为词袋】的简单说明
Python 机器学习 基础 之 处理文本数据 【处理文本数据/用字符串表示数据类型/将文本数据表示为词袋】的简单说明 目录 Python 机器学习 基础 之 处理文本数据 【处理文本数据/用字符串表示数据类型/将文本数据表示为词袋】的简单说明 一、简单介绍 二、处理文本数据 三、用…...

GAT1399协议分析(10)--视频定义及解析
一、官方定义 二、字段解析 VideoID 类型BasicObjectID 解析参考GAT1399协议分析(8)--ImageInfo字段详解-CSDN博客 InfoKind 采集类型...

【C语言】学生管理系统:完整模拟与实现
🌈个人主页:是店小二呀 🌈C语言笔记专栏:C语言笔记 🌈C笔记专栏: C笔记 🌈喜欢的诗句:无人扶我青云志 我自踏雪至山巅 🔥引言 本篇文章为修改了在校期间实训报告,使用C…...

pypi 发布自己的包
注册pypi个人用户 网址:https://pypi.org 目录结构dingtalk_utils 必须-pkgs- __init__.py .gitignore LICENSE 必须 README.md 必须 requirements.txt setup.py 必须安装依赖 pip install setuptools wheel安装上传工具 pip install twinesetup.py i…...

关闭windows11磁盘地址栏上的历史记录
关闭windows11的磁盘地址栏上的历史记录 windows11打开磁盘后访问某一个磁盘路径后会记录这个磁盘路径,而且有时候会卡住这个地址栏(关都关不掉),非常麻烦。 如下图所示: 关闭地址栏历史记录 按下windows键打开开…...

DDS自动化测试落地方案 | 怿星科技携最新技术亮相是德科技年度盛会
5月28日,怿星科技作为是德科技的重要合作伙伴亮相Keysight World Tech Day 2024。在此次科技盛会上,怿星科技不仅展示了领先的DDS自动化测试解决方案等前沿技术,还分享了在“周期短、任务重”的情况下,如何做好软件开发和测试验证…...

新品!和芯星通全系统全频高精度板卡UB9A0首发
6月6日,和芯星通发布了UB9A0全系统全频高精度GNSS板卡,主要应用于CORS站、便携基站、GNSS全球监测跟踪站等。延续了上一代产品高质量原始观测量的特点,UB9A0在性能和稳定性方面均表现出众。 UB9A0基于射频基带及高精度算法一体化的GNSS SoC芯…...

Cognita RAG:模块化、易用与可扩展的开源框架
Cognita RAG是一个开源框架,它通过模块化设计、用户友好的界面和可扩展性,简化了将领域特定知识整合到通用预训练语言模型中的过程。本文介绍了Cognita的特点、优势、应用场景以及如何帮助开发者构建适合生产环境的RAG应用程序。 文章目录 Cognita RAG介…...

linux虚拟机免密登录配置
1、假设A服务器要免密登录B服务器 2、在A服务器上执行命令: cd /root/.ssh/ ssh-keygen -t rsa #这里会生成两个文件 一个是id_rsa私钥和公钥rsa.pub2、我们把公钥的内容复制粘贴到B服务器的/root/.ssh/authorized_keys文件下 #在A服务器上执行命令记录内容 cat …...

Qt_C++ RFID网络读卡器Socket Udp通讯示例源码
本示例使用的设备: WIFI/TCP/UDP/HTTP协议RFID液显网络读卡器可二次开发语音播报POE-淘宝网 (taobao.com) #ifndef MAINWINDOW_H #define MAINWINDOW_H#include <QMainWindow> #include <QHostInfo> #include <QNetworkInterface> #include <…...

C++ 实现Python 列表list 的两种方法
1、vector里面放多种参数。在C中,如果你想要在std::vector中存储不同类型的参数,你可以使用std::any(C17及以上)或std::variant(C17以前的版本需要使用Boost库或者C17及以上标准)。以下是使用std::vector&l…...

vue3+ elementPlus PC端开发 遇到页面已进入就form校验了的问题
form表单一进页面就校验了 rules里配置的 require 提示语 如图所示代码是这样的 最后发现是form表单下面的一个按钮的展示规则 会导致规则校验 canAddInsured 这个字段的变化会导致form表单校验 这个字段是computed maxInsureds 也是个computed监听 maxInsured.value >1 就…...

transformers DataCollator介绍
本博客主要介绍 transformers DataCollator的使用 from transformers import AutoTokenizer, AutoModel, \DataCollatorForSeq2Seq, DataCollatorWithPadding, \DataCollatorForTokenClassification, DefaultDataCollator, DataCollatorForLanguageModelingPRETRAIN_MODEL &qu…...

rust学习(字节数组转string)
最新在写数据传输相关的操作,发现string一个有趣的现象,代码如下: fn main() {let mut data:[u8;32] [0;32];data[0] a as u8;let my_str1 String::from_utf8_lossy(&data);let my_str my_str1.trim();println!("my_str len is…...

Docker:技术架构演进
文章目录 基本概念架构演进单机架构应用数据分离架构应用服务集群架构读写分离/主从分离架构冷热分离架构垂直分库微服务容器编排架构 本篇开始进行对于Docker的学习,Docker是一个陌生的词汇,那么本篇开始就先从技术架构的角度出发,先对于技术…...

汽车MCU虚拟化--对中断虚拟化的思考(2)
目录 1.引入 2.TC4xx如何实现中断虚拟化 3.小结 1.引入 其实不管内核怎么变,针对中断虚拟化无非就是上面两种,要么透传给VM,要么由Hypervisor统一分发。汽车MCU虚拟化--对中断虚拟化的思考(1)-CSDN博客 那么,作为车规MCU龙头…...