volatile如何保证可见性和有序性

1.Volatile保证可见性的原理

当JVM将.class文件编译为具体的CPU执行指令(也就是机器码)后,观察这些指令,我们会发现只要是加了volatile修饰的共享变量,都会在指令前面加上一个以lock为前缀的指令,lock 前缀的指令会引发两件事情:

  1. 将当前处理器缓存行的数据写回到系统内存
  2. 一个处理器的缓存回写到内存会导致其他处理器的缓存失效

       第一点的实现:lock信号一般不锁总线,而是锁缓存,毕竟锁总线开销的比较大。如果访问的内存区域没有缓存在处理器内部,它会锁定这块内存区域的缓存并回写到内存,并使用缓存一致性机制来确保修改的原子性,此操作被称为“缓存锁定”,缓存一致性机制会阻止同时修改由两个以上处理器缓存的内存区域数据。

       第二点的实现:IA-32 CPU和 Intel 64 CPU使用 MESI(修改、独占、共享、无效)控制协议去维护内部缓存和其他处理器缓存的一致性,避免在总线加lock锁。CPU使用嗅探技术保证它的内部缓存、系统内存和其他处理器的缓存的数据在总线上保持一致。具体解决思路为:当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,那么他会发出信号通知其他CPU将该变量的缓存行设置为无效状态。当其他CPU使用这个变量时,首先会去嗅探是否有对该变量更改的信号,当发现这个变量的缓存行已经无效时,会重新从内存中读取这个变量

以上就是volatile内部保证可见性的实现原理

2.Volatile保证有序性的原理

为了性能优化,JVM会在不改变数据依赖性的情况下,允许编译器和处理器对指令序列进行重排序,而有序性问题指的就是程序代码执行的顺序与程序员编写程序的顺序不一致,导致程序结果不正确的问题。而加了volatile修饰的共享变量,则通过内存屏障解决了多线程下有序性问题。

为什么指令重排序可以提⾼性能?
       简单地说,每⼀个指令都会包含多个步骤,每个步骤可能使⽤不同的硬件。因此,流⽔线技术产⽣了,它的原理是指令1还没有执⾏完,就可以开始执⾏指令2,⽽不⽤等到指令1执⾏结束之后再执⾏指令2,这样就⼤⼤提⾼了效率。

       但是,流⽔线技术最害怕中断,恢复中断的代价是⽐较⼤的,所以我们要想尽办法不让流⽔线中断。指令重排就是减少中断的⼀种技术。我们分析⼀下下⾯这个代码的执⾏情况:

a = b + c;
d = e - f ;

       先加载b、c(注意,即有可能先加载b,也有可能先加载c),但是在执⾏add(b,c)的时候,需要等待b、c装载结束才能继续执⾏,也就是增加了停顿,那么后⾯的指令也会依次有停顿,这降低了计算机的执⾏效率。

       为了减少这个停顿,我们可以先加载e和f,然后再去加载add(b,c),这样做对程序(串⾏)是没有影响的,但却减少了停顿。既然add(b,c)需要停顿,那还不如去做⼀些有意义的事情。

综上所述,指令重排对于提⾼CPU处理性能是有效果的。不过虽然指令重排可以保证串⾏语义⼀致,但是没有义务保证多线程间的语义也⼀致。所以在多线程下,指令重排序可能会导致⼀些问题。

那又什么是内存屏障呢?
具体来讲,内存屏障分为以下四类:

注:下述Load代表读操作,Store代表写操作

屏障类型指令示例说明
LoadLoad BarriesLoad1;LoadLoad;Load2确保Load1数据的装载先于Load2以及后续装载指令的装载。
StoreStore BarriesStore1;StoreStore;Store2确保Store1数据刷新到内存先于Store2以及后续存储指令的存储。
LoadStore BarriesLoad1;LoadStore;Store2确保Load1数据的装载先于Store2数据刷新到内存以及后续存储指令的存储。
StoreLoad BarriesStore1;StoreLoad;Load2确保Store1数据刷新到内存先于Load2数据的装载以及后续装载指令的装载。

编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。
编译器选择了⼀个⽐较保守的JMM内存屏障插⼊策略,这样可以保证在任何处理器平台,任何程序中都能得到正确的volatile内存语义。这个策略是:

*在每个volatile写操作的前面插入一个StoreStore屏障,禁止StoreStore屏障前面的普通写操作和volatile写操作重排序。
* 在每个volatile写操作的后面插入一个StoreLoad屏障,禁止StoreLoad屏障后面可能有的读操作和volatile写操作重排序。
* 在每个volatile读操作的后面插入一个LoadLoad屏障,禁止volatile读操作和LoadLoad屏障后面的普通读操作重排序。
* 在每个volatile读操作的后面插入一个LoadStore屏障,禁止volatile读操作和LoadStore屏障后面的普通写操作重排序。

大致示意图如下:
在这里插入图片描述

在这里插入图片描述

参考《深入浅出Java多线程》

  • 12
    点赞
  • 55
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值