package com.crazymakercircle.visiable;
public class VolatileVar{
//使用volatile保障内存可见性
volatile int var = 0;
public void setVar(int var){
System.out.println("setVar = " + var);
this.var = var;
}
public static void main(String[] args){
VolatileVar var = new VolatileVar();
var.setVar(100);
}
}
===> 汇编指令:
0x0000000003931be6: mov %r8d,0xc(%rdx)
0x0000000003931bea: lock addl $0x0,(%rsp);*putfield var;
- ..VolatileVar::setVar@27 (line 17)
0x000000000305016f: add $0x50,%rsp
0x0000000003050173: pop %rbp
由于共享变量var加了volatile关键字,因此在汇编指令中,操作var之前多出一个lock前缀指令lock addl,该lock前缀指令有三个功能:
加了volatile关键字修饰的变量,只要有一个线程将主内存中的变量值做了修改,其他线程都将马上收到通知,立即获得最新值。当写线程写一个volatile变量时,JMM会把该线程对应的本地工作内存中的共享变量值刷新到主内存。当读线程读一个volatile变量时,JMM会把该线程对应的本地工作内存置为无效,线程将到主内存中重新读取共享变量。
volatile语义实现原理:
两个与CPU相关的专业术语:
volatile可见性的实现是借助了CPU的lock指令,lock指令在多核处理器下,可以将当前处理器的缓存行的数据写回到系统内存,同时使其他CPU里缓存了该内存地址的数据置为无效。通过在写volatile的机器指令前加上lock前缀,使写volatile具有以下两个原则:
volatile关键字除了保障内存可见性外,还能确保执行的有序性。volatile语义中的有序性是通过内存屏障指令来确保的。为了实现volatile关键字语义的有序性,JVM编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序,JMM建议JVM采取保守策略对重排序进行严格禁止,下面是基于保守策略的volatile操作的内存屏障插入策略。
volatile写操作的内存屏障插入策略为:在每个volatile写操作前插入StoreStore(SS)屏障,在写操作后面插入StoreLoad屏障;volatile读操作的内存屏障插入策略为:在每个volatile写操作后插入LoadLoad(LL)屏障和LoadStore屏障,禁止后面的普通读、普通写和前面的volatile读操作发生重排序;
对于关键字volatile修饰的内存可见变量而言,具有两个重要的语义:
(1)使用volatile修饰的变量在变量值发生改变时,会立刻同步到主存,并使其他线程的变量副本失效;
(2)禁止指令重排序:用volatile修饰的变量在硬件层面上会通过在指令前后加入内存屏障来实现,编译器级别是通过下面的规则实现的;
JMM对于volatile变量会有特殊的约束:
(1)使用volatile修饰的变量其read、load、use都是连续出现的,所以每次使用变量的时候都要从主存读取最新的变量值,替换私有内存的变量副本值(如果不同的话);
(2)其对同一变量的assign、store、write操作都是连续出现的,所以每次对变量的改变都会立马同步到主存中;
假设有两个线程A、B分别运行在Core1、Core2上,并假设此时的value为0,线程A、B也都读取了value值到自己的工作内存;
现在线程A将value变成1之后,完成了assign、store的操作,假设在执行write指令之前,线程A的CPU时间片用完,线程A被空闲,但是线程A的write操作没有到达主存。由于线程A的store指令触发了写的信号,线程B缓存过期,重新从主存读取到value值,但是线程A的写入没有最终完成,线程B读到的value值还是0。线程B执行完成所有的操作之后,将value变成1写入主存。线程A的时间片重新拿到,重新执行store操作,将过期了的1写入主存。线程A、B并发操作value时可能发生脏数据写入的流程,在高并发场景下,volatile变量一定需要使用Java的显式锁结合使用;