volatile特性 :有序、可见、不保证原子性
内存语义:当写一个volatile变量时,jmm会把该线程对应的本地内存中的共享变量值立即刷新到主内存中。
当读一个volatile变量时,jmm会把该线程对应的本地内存设置成无效,重新回到主内存中读取最新共享变量
所以volitale的写内存语义是直接刷新到主内存中,读的内存语义是直接从主内存中读取。
volatile凭什么保证可见性和有序性呢?主要是内存屏障 memory barrier
volatile可见性
写完后立即刷新回主内存并及时发出通知,大家可以去主内存拿最新的值,前面的修改对后面所有的线程都是可见的。
有序性(禁止重排序)
重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段,有时候会改变程序语句的先后顺序,不存在数据依赖关系,可以重新排序。
存在数据依赖关系,禁止重新排序
但是重排后决定不能改变原有的串行语义。
内存屏障是什么
内存屏障 (也叫内存栅栏、屏障指令等 是一类同步屏障指令,是cpu或编译器在对内存随意访问的操作中的一个同步点,使得此点之前的所有读写操作都执行后才可以开始执行此点之后的操作),避免代码重排序。内存屏障其实是一种jvm指令,java内存模型的重排序规则会要求java编译器在生成jvm指令时插入特定的内存屏障指令,通过这些内存屏障指令,volatile实现了jmm中的可见性和有序性 (禁止重排序),volatile没有原子性
内存屏障之前的所有写操作都要写回主内存,
内存屏障之后的所有读操作都能获得内存屏障之前的所有写操作的最新结果(实现了可见性)
写屏障 告诉处理器在写屏障之前将所有存储在缓存(store bufferes)中的数据同步到主内存。也就是说当看到store屏障指令,就必须把该指令之前所有写入指令执行完毕才能继续往下执行
读屏障 处理器在读屏障之后的读操作,都在读屏障之后执行,也就是说在load屏障指令之后就能够保证后面的读取数据指令一定能够读取最新的数据
因此重排序时,不允许把内存屏障之后的指令重排到内存屏障之前,一句话 对一个volatile变量的写先行发生于任意后续对这个volatile变量的读,也叫写后读
读屏障 在读指令之前插入读屏障,让工作内存或cpu高速缓存当中的缓存数据失效,重新回到主内存中获取最新数据
写屏障 在写指令之后插入写屏障,强制把写缓存区的数据刷回到主内存中。
内存屏障细分四种
happens-before之volatile变量规则
jmm将内存屏障插入策略分为四种规则
读屏障
在每个volatile读操作的后插入一个loadload 屏障
在每个volatile读操作的后插入一个loadstore屏障
volatile读插入内存屏障后生成的指令序列图如下
写屏障
在每个volatile写操作的前插入一个storestore 屏障
在每个volatile读操作的后插入一个storeload屏障
volatile写插入内存屏障后生成的指令序列图如下
volatile可见性
保证不同线程对某个变量完成操作后结果及时可见,即共享变量一但改变所有线程都可见。
多线程中操作i++不保证原子性
从字节码角度分析
volatile适合的场景
指令禁重排
四大屏障插入的情况
上述理论用代码实现
volatile的使用场景
在 while 中不能添加 System.out.println() 或者日志输入,以及线程睡眠 sleep() 等方法,
否则 flag不用 volatile 修饰也会结束,比如 System.out.println() 看源码发现如下:
public void println(String x) {
synchronized (this) {
print(x);
newLine();
}
}
synchronized 具有线程可见性,会刷新主内存数据,其他线程能获取到最新数据
面试总结部分
volatile的可见性
volatile禁止重排
写指令
读指令
凭什么我加个volatile关键字,系统底层就会加内存屏障?两者有什么关系,从字节码角度分析
内存屏障是什么
内存屏障能干什么
内存屏障的四大指令
三句话总结