提起Volatile关键字,相信大家并不陌生,应该每个面试的时候都被问到过:说说你对Volatile关键字的理解?
一般我们这么回答: Volatile 是用于在并发环境中保证该变量的内存可见性和禁止指令重排的. 那么Volatile底层究竟是如何保证以上两点的呢? 再看底层之前,先讲一下JMM内存模型
每个线程持有的工作内存中的变量是主内存中共享变量的副本,主内存和工作内存中的数据通过原子操作来完成,当线程2的value++时,再将value=1的新值store之前,线程1 是不知道的,执行store后,会将新的数据write回主内存,总线通过嗅探机制,发现这个变量被修改过了,会通知线程1 将工作内存中的value缓存失效掉,重新都主内存 read load.
看懂上面这幅图了,来看下下面这段代码
两个线程运行,分别输出语句,flag初始值为false,线程1 中进行while判断当!flag时,会打印步骤执行了这句话,根据打印结果可以看出,已经输出了执行修改成功,flag已经等于true了,但是 并没有输出步骤执行了,证明线程2 对flag的修改对线程1 是不可见得,我们将flag用volatile修饰一下,再看下打印结果
看下步骤执行了这句话已经输出,代表线程2 的修改已经对线程1 可见了,那么volatile究竟是怎么实现这一点的呢?
因为Volatile是有c++,底层是汇编语言实现的,所以我们想看汇编指令了话要下载
hsdis-amd64.dll这个文件放到jre的bin目录下,在VM加上启动命令参数 -server -Xcomp -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XX:CompileCommand=compileonly,*VoatileTest.changeFlag,这里百度一下就可以,有教程,再次看下程序运行结果
可以看到,在我们对flag进行赋值的时候,底层会给加一个lock指令,不加volatile修饰的是没有lock前缀的,lock指令做了哪些事?百度一下intel的汇编手册可以发现
1.会将当前处理器缓存行的数据立即回写到主内存 2.回写操作会引起在其他CPU里缓存该内存地址的数据无效(MESI协议) 3.提供内存屏障功能,使lock前后指令不能重排序
1、2 两点就保证了Volatile的内存可见性,name禁止指令重排如何验证呢?看下下面这段代码
public class VolatileTest {
static int x=0,y=0;
static int a=0,b=0;
public static void main(String[] args) throws InterruptedException {
Set<String> hashSet =new HashSet<>();
for(int i=0;i<1000000;i++){
x=0;
y=0;
a=0;
b=0;
Thread thread1= new Thread(()->{
a=y;//3
x=1;//1
});
Thread thread2= new Thread(()->{
b=x;//4
y=1;//2
});
thread1.start();;
thread2.start();
thread1.join();
thread2.join();
hashSet.add("a="+a+",b="+b);
System.out.println(hashSet);
}
}
}
程序运行的结果一共有四种 [a=0,b=0, a=1,b=0, a=0,b=1, a=1,b=1],前三种都比较好理解,第四种按照正常逻辑是不可能出现这种结果的,如果要出现a=1,b=1的结果 CPU得按照上面 1234的步骤去执行才行,代码可以自己拷贝跑一下(最后一种出现的概率极低,多跑几次),那么是不是指令执行的顺序被重新排序了?计算机在 不影响单线程执行结果的情况下,为了最大限度发挥机器的性能,会对机器指令做重排序优化,重排序会遵循 as-if -serial 和 happens-before原则,这两个原则和MESI就不说了,想了解的话可以去百度一下
总结: 面试题:谈谈你对volatile关键字的理解?
答:volatile关键字 是用于在并发环境中保证该变量的内存可见性和禁止指令重排的,可见性原理是 被volatile修饰的变量在被其中一个线程修改时,汇编层会在变量产生变化的操作前增加lock指令 去锁定当前内存的缓冲区并会将当前处理器缓存行的数据立即回写到主内存,回写操作会引起在其他CPU里缓存该内存地址的数据无效(MESI协议), 禁止指令重排也是通过检测lock前缀后,在被ock的指令前后设置内存屏障来禁止特定类型的指令执行
感谢收看,欢迎批评指正