一、 什么是内存可见性问题?
可见性问题是指, 一个线程对共享变量值的修改,不能够及时地被其他线程看到
二、 主内存与工作内存?
提到内存可见性,我们就可能会听到工作内存以及主内存这两个概念... 这俩是什么?它们和内存可见性问题又有什么联系?
首先要明确:CPU是无法直接对内存中的数据进行计算的,cpu的计算分为三个步骤:
- 从内存中读取数据存入到寄存器中
- 在寄存器中进行计算
- 将寄存器中计算的结果传回内存
JVM在设计的时候,对底层的CPU和内存的关系进行了一个抽象:
主内存:计算机真正的内存
工作内存:对应于CPU中的寄存器和缓存等等。由于有多核的存在,这个工作内存也会有多个存在。这个概念和线程私有的内存还是不一样的,线程私有的内存,其实是栈内存。
涉及到主存和工作内存的问题,多半是和多线程在多个实际的CPU核心中运行的场景中才讨论的。
正是由于工作内存的存在,而且可能是分布在不同的CPU核心中,有了数据的“内存可见性问题
🔊 来看一个实例:
该代码中有两个线程,线程1进行循环操作,线程2控制线程1的循环。希望达到 “ 当用户输入非0数字时,线程1退出循环,退出进程” 的效果
import java.util.Scanner;
public class demo3 {
public static int flag=0;
public static void main(String[] args) {
Thread thread1=new Thread(()->{
while(flag==0){
//执行循环,循环内部什么都不做
}
//一旦退出循环,打印循环结束
System.out.println("循环结束");
});
thread1.start();
Thread thread2=new Thread(()->{
//让用户输入一个非0数字退出循环
System.out.println("请输入一个非0数字,退出线程1的循环:");
Scanner scanner=new Scanner(System.in);
flag=scanner.nextInt();
});
thread2.start();
}
}
代码结果:当用户输入非0数字时,进程并没有结束
原因分析:
线程1不断地进行读取/判断数据
由于从主内存中读取数据的过程耗费的时间>从工作内存(寄存器等)中读取数据的时间,当多次循环判断,操作系统/jvm编辑器发现每次写回主内存的数据并没有修改过的时候,就会进行优化: 既然反复读内存读到的数据都一样,干脆将读到的值存到寄存器中,下次直接读寄存器就好了。
于是经过优化后,线程1不再进行重复读。
麻烦的事儿来了!如果此时的线程2对主内存中的数据进行了修改,线程1就没办法感受到主内存中数据的变化。这就是内存可见性问题。
三、使用volatile解决问题
内存可见性问题本质上是操作系统/jvm对计算过程优化后带来的问题,我们在待修改的变量前使用volatile,其实相当于显示的禁止了这种优化。
在待修改的变量前加上 volatile ,是给这个相应的变量加上了“内存屏障”(特殊的二进制指令)。jvm在读取这个变量的时候,由于内存屏障的存在,就知道每次都需要重新从主内存中读取数据而不是草率的进行优化。
对刚刚的实例进行修改, 只需要在变量前增加volatile关键字:
这一次,当我们输入非0数字的时候,进程就如我们所愿结束了...
不过:
如果我们在线程1的循环内存加上个sleep,编辑器好像就不会优化了,程序也能如愿结束。编辑器的优化是个玄学问题,什么时候会优化我们无从确定。为了保险起见,有必要的时候我们还是加上volatile吧 ~