引入
学习这个之前,先来看这样一段代码:
import java.util.Scanner; public class ThreadTest3 { public static int flag = 0; public static void main(String[] args) { Thread t1 = new Thread(() -> { while(flag == 0) { //循环体里,啥都不写 } System.out.println("线程结束"); }); Thread t2 = new Thread(() -> { Scanner sc = new Scanner(System.in); flag = sc.nextInt(); }); t1.start(); t2.start(); } }
运行一下,看一下结果:
按理来说:当修改flag的值之后,就应该不满足t1的条件了,t1就会结束.但显然结果不是这样的,让我们来分析一下原因:
这主要是因为两条核心指令:
1.load读取内存中的flag放到cpu的寄存器里.
2.拿着寄存器的值和0比较(条件跳转指令)
主要就是两个要点:
1.由于一开始flag为1,一开始load操作的执行结果,都是将0放入到寄存器中,但如果是输入的值的话,可能就是好几秒之后了,而这几秒钟的load操作,可能就很多了(上百亿次)
2.load的操作开销远远超过条件跳转指令(因为访问寄存器操作速度远远高于内存,就会把load优化掉)
因为编译器发现,每次循环都要读取内存,开销太大,于是将读取内存的操作优化成读取寄存器的操作.
由上,可以发现输入的值是存入到了内存里面,而while哪里判断使用的flag一直都是从寄存器中读取的.因此输入的值并没有任何影响,那么有什么方法能够解决这种问题呢?这就用到了volatile关键字.
volatile关键字
核心:volatile能够保证内存可见性
volatile修饰的变量,能够保证"内存可见性".(强制读取内存),这就解决上面的问题了
禁止指令重排序,针对这个被volatile修饰的变量的的读写操作相关指令,是不可以重排序的.
让我们看一下修改的代码及结果
public class ThreadTest3 { public static volatile int flag = 0; public static void main(String[] args) { Thread t1 = new Thread(() -> { while(flag == 0) { //循环体里,啥都不写 //啥都不写可能一秒内能运行上百亿次1 //这时被优化的迫切程度就很高 //譬如Thread.sleep(1000)的操作可以将运行次数优化到1000次 //这时优化的操作就不必要了 } System.out.println("线程结束"); }); Thread t2 = new Thread(() -> { //相当于t2改变了线程,但是t1没有看见内存的变化 Scanner sc = new Scanner(System.in); flag = sc.nextInt(); }); t1.start(); t2.start(); } }
显然解决了问题.
代码在写入volatile关键字的时候,
1.改变线程工作内存中volatile副本的值
2.将改变后的副本的值从工作内存刷新到主内存
代码在读取volatile修饰变量的时候,
1.从主内存中读取volatile关键字修饰的最新值到线程的工作内存中
2.从工作内存中读取volatile关键字的副本
前面我们讨论内存可见性时就说了`,直接访问工作内存(实际是CPU的寄存器或者CPU的缓存),速度非常快,但是可能出现数据不一致的情况.
加上volatile,强制读写内存,速度变慢了,但是数据更准确了.