内存可见性问题

news/2024/11/8 14:57:26/

目录

1.什么是内存可见性问题

2.内存可见性问题是怎么发生的

3.解决方法:volatile

4.volatile使用的注意事项

5.内存可见性问题的延伸

缓存(cache)


1.什么是内存可见性问题

首先来看一段代码

class Counter{public int flag = 0;
}
public class VolatileDemo1 {public static void main(String[] args) {Counter counter = new Counter();Thread t1 = new Thread(() -> {while(counter.flag == 0) {//循环里面不进行任何操作}System.out.println("t1 循环结束");});Thread t2 = new Thread(() -> {Scanner scanner = new Scanner(System.in);System.out.print("请输入flag: ");counter.flag = scanner.nextInt();});t1.start();t2.start();try {t1.join();t2.join();} catch (InterruptedException e) {e.printStackTrace();}}
}

这段代码一共创建了两个线程,其中t1线程去判断flag的值(默认为0),如果不为0则跳出循环(循环里面不执行任何操作)当flag不为0时,提示t1线程结束,t2线程则是输入一个值,赋给flag。

按照我们的逻辑,当t2线程输入一个不为0的数字时,t1线程会打印“t1 循环结束”,那么我们来看一下结果,如下:

 可以看到,我们输入1,赋值给flag,但是t1循环却没有对此做出相应的操作,这就是出现了内存可见性问题。

2.内存可见性问题是怎么发生的

首先针对上面的例子,我们做一些分析。

t1线程中有一个循环,循环条件是判断flag这个变量是否为0,循环体为空

t2线程是输入一个数字赋值给flag

按照逻辑当t2输入数字不为0,那么t1循环结束,那么为什么当t2输入了一个不为0的数字时,t1循环仍然没有结束呢?

可以肯定的是:t2中的输入和赋值操作都是没有问题的,那么问题的所在就一个在t1的身上。

那么我们对t1中的执行语句做一些分析:

t1线程中储粮打印操作,唯一可以被执行的计算循环的判断条件 counter.flag == 0 。

这条语句我们可以把它拆分成两条指令:

一条是从内存中获取flag的值--load

一条是将这个值和0进行比较--cmp

按理来说,如果每次进入循环条件判断的时候,都对flag的值进行获取,那么结果就不会出现死循环的现象,而此时出现了死循环,那么就说明对flag的获取出现了问题。

t1中的这个循环是空体,这个循环在执行时的速度极快,1秒钟可以执行上百万次,而执行了这么多次load的获取结果都是一样的。另一方面,load的执行速度相比于cmp慢了太多了。此时JVM就做出来一个非常大胆的决定--不再真正的去重复load了,因为判定好像没人去修改flag的值,所以干脆就只获取一次就好了,此时就出现了前面运行的情况了。

上述的这种情况是编译器优化的一种方式,而内存可见性问题归根结底就是编译器/JVM在多线程环境下优化时产生了误判,此时就需要我们去手动干预,让编译器不要瞎搞,而这个操作结束在变量前面加上 volatile 关键字。

3.解决方法:volatile

继续挪用上面的代码,并且给flag这个变量加上volatile

class Counter{volatile public int flag = 0;
}
public class VolatileDemo1 {public static void main(String[] args) {Counter counter = new Counter();Thread t1 = new Thread(() -> {while(counter.flag == 0) {//循环里面不进行任何操作}System.out.println("t1 循环结束");});Thread t2 = new Thread(() -> {Scanner scanner = new Scanner(System.in);System.out.print("请输入flag: ");counter.flag = scanner.nextInt();});t1.start();t2.start();try {t1.join();t2.join();} catch (InterruptedException e) {e.printStackTrace();}}
}

此时再去运行可以看到

加了volatile之后,代码的运行情况就符合我们的预期了。

当然,代JVM并不是任何时候都会出现优化误判的情况,比如下面的代码

class Counter{public int flag = 0;
}
public class VolatileDemo1 {public static void main(String[] args) {Counter counter = new Counter();//编译器不是任何时候都会进行优化或者优化出错 如下,即使没有 volatile 也可以正常运行Thread t1 = new Thread(() -> {while(counter.flag == 0) {//循环里面不进行任何操作try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}}System.out.println("t1 循环结束");});Thread t2 = new Thread(() -> {Scanner scanner = new Scanner(System.in);System.out.print("请输入flag: ");counter.flag = scanner.nextInt();});t1.start();t2.start();try {t1.join();t2.join();} catch (InterruptedException e) {e.printStackTrace();}}
}

我们在循环体中加入了sleep,此时代码中没有加 volatile 但是代码也可以正常运行,但是这开发中,对于这种不确定的情况,还是加上volatile更加稳妥。

4.volatile使用的注意事项

volatile 只可以对变量进行修饰,不可以对方法进行修饰。

volatile 不可以对方法中的局部变量进行修饰。

volatile 不保证原子性,若想保证原子性要使用 synchronized 

5.内存可见性问题的延伸

关于内存可见性问题,还可以从JMM(Java Memory Modle java内存模型)的角度去重新表述

Java程序里除了主内存,每个线程还有自己的“工作内存”

t1线程进行读取的时候只是读取了它工作内存的数据

t2线程进行修改的时候,先修改工作内存的数据,然后再把工作内存的数据同步到主内存中,但是由于编译器优化,导致t1没有重新从主内存中同步数据到它的工作内存中,所以读到的结果就是错误的结果。(主内存和工作内存这样的表述来自于Java文档)

上面的主内存既可以理解为前面说的内存;

而工作内存可以理解为工作存储区,也就是CPU上存储数据的单元(寄存器)以及缓存。

缓存(cache)

CPU中的寄存器存储的空间小,读写速度快,成本高;

内存的存储空间大,读写速度慢,成本低(相对于寄存器来说)

缓存就是他俩的中间值,缓存存储空间居中,读写速度居中,成本居中

当cpu在读取一个数据的时候,可能是直接读取内存,也可能是读取缓存,还可能是读取寄存器

前面说的工作内存,之所以将寄存器和缓存都包含进去,一方面是因为描述简单,另一方面,无论是缓存还是寄存器都不会对我们得到的结论产生影响。


http://www.ppmy.cn/news/2062.html

相关文章

免费分享20套微信小程序源码 源码免费下载【强烈推荐】

淘源码:国内知名的源码免费下载平台 微信小程序源码包括:商城系统、点餐外卖、垃圾分类、预约洗车、物业管理、校园跑腿、驾考学习、会议预约、图书管理、智能停车、在线答题等小程序源码。 源码分享,文末获取源码! 1、JAVA微信…

并查集介绍

文章目录:并查集原理并查集实现并查集的类结构并查集的合并统计集合数量并查集原理 在一些应用问题中,需要将 n 个不同的元素划分成一些不相交的集合。开始时,每个元素自成一个单元素集合,然后按照一定的规律将归于同一组元素的集…

ASP.NET Core 3.1系列(18)——EFCore中执行原生SQL语句

1、前言 前一篇博客介绍了EFCore中常见的一些查询操作,使用Linq或Lambda结合实体类的操作相当方便。但在某些特殊情况下,我们仍旧需要使用原生SQL来获取数据。好在EFCore中提供了完整的方法支持原生SQL,下面开始介绍。 2、构建测试数据库 …

【Opencv实战】高手勿入,Python使用Opencv+Canny实现边缘检测以及轮廓检测(详细步骤+源码分享)

前言 有温度 有深度 有广度 就等你来关注哦~ 所有文章完整的素材源码都在👇👇 粉丝白嫖源码福利,请移步至CSDN社区或文末公众hao即可免费。 在这次的案例实战中,我们将使用Python 3和OpenCV。我们将使用OpenCV,因为它是…

C# 11 中的新增功能

我们很高兴地宣布 C# 11 已经发布!与往常一样,C# 开辟了一些全新的领域,同时推进了过去版本中一直在运行的几个主题。我们的文档页面上的 C# 11 的新增功能下有许多功能和详细信息,这些内容都得到了很好的介绍。 随着每个版本的发…

数据结构 树练习题

目录 判断 选择 判断 1.一棵有124个结点的完全二叉树,其 叶结点个数是确定的。 【答案】正确 【解析】完全二叉树 若设二叉树的深度为h 除第 h 层外 其它各层 1~(h-1) 的结点数都达到最大个数(即1~(h-1)层为一个满二叉树) 第 h 层所有的结点都连续集…

python 基础之垃圾回收机制

一、背景 之前能说个大概,python垃圾回收机制,设计到细节就不太清楚。 如同刚毕业的少年,出厂自带三年工作经验。做过啥啥.. 一问细节,阿西吧.. 不要问我怎么知道滴.. 哈哈!!!- 提高自己的计算机基础 - 重要的是面试(曾被问到三…

m基于遗传优化的不同等级电动汽车充电站的选址方案matlab仿真

目录 1.算法描述 2.仿真效果预览 3.MATLAB核心程序 4.完整MATLAB 1.算法描述 作为电动汽车的普及与推广,必要的基础配套服务设施、充电站的建设位置和选址规划对整体行业的发展起着重要的意义,本文中提出了一个不同等级电动汽车充电站的选址与求解算…