内存可见性问题
创始人
2024-03-22 11:41:04
0

目录

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在读取一个数据的时候,可能是直接读取内存,也可能是读取缓存,还可能是读取寄存器

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

相关内容

热门资讯

监控摄像头接入GB28181平... 流程简介将监控摄像头的视频在网站和APP中直播,要解决的几个问题是:1&...
Windows10添加群晖磁盘... 在使用群晖NAS时,我们需要通过本地映射的方式把NAS映射成本地的一块磁盘使用。 通过...
protocol buffer... 目录 目录 什么是protocol buffer 1.protobuf 1.1安装  1.2使用...
educoder数据结构与算法...                                                   ...
MySQL下载和安装(Wind... 前言:刚换了一台电脑,里面所有东西都需要重新配置,习惯了所...
MFC文件操作  MFC提供了一个文件操作的基类CFile,这个类提供了一个没有缓存的二进制格式的磁盘...
有效的括号 一、题目 给定一个只包括 '(',')','{','}'...
【Ctfer训练计划】——(三... 作者名:Demo不是emo  主页面链接:主页传送门 创作初心ÿ...
Fluent中创建监测点 1 概述某些仿真问题,需要创建监测点,用于获取空间定点的数据࿰...
带头循环双向链表来咯!!! 前言:继上文,我们了解了结构最简单的一种链表---单链表那么我们今天就来...