volatile
目标
1、volatile如何保证内存可见性
2、volatile如何禁止指令重排序
3、内存屏障
4、内存可见性
5、关于volatile的单例模式
一、内存可见性
1.1 缓存一致性问题
1、现代计算机系统在存储设备与处理器之间加了一层读写速度尽可能解决处理器运算速度的高速缓存来作为内存与处理器之间的缓冲: 将运算需要使用到的数据复制到缓存中, 让运算能快速进行, 当运算结束后再从缓存同步回内存之中, 这样处理器就无须等待缓慢的内存读写.
2、缓存一致性问题:
在多处理器系统中, 每个处理器都有自己的高速缓存, 而它们又共享同一主内存, 当多个处理器的运算任务都涉及到同一个块主内存区域时, 将可能导致各自的缓存数据不一致.
1.2 内存模型
在特定的操作协议下, 对特定的内存或高速缓存进行读写访问的过程抽象.
1.3 内存可见性
1、一个CPU核心对数据的修改, 对其他CPU核心立即可见.
2、CPU修改数据, 首先是对缓存的修改, 然后再同步回主存, 在同步回主存的时候, 如果其他CPU也缓存了这个数据, 就会导致其他CPU缓存上的数据失效, 这样当其他CPU再去它的缓存读取这个数据的时候, 就必须从主存重新获取.
3、实现原理一般是基于CPU的MESI协议
, 其中E表示独占Exclusive, S表示Shared, M表示Modify, I表示Invalid, 如果一个CPU核心修改了数据, 那么这个CPU核心的数据状态就会更新为M, 同时其他核心上的数据状态更新为I, 这个是通过CPU多核之间的嗅探机制实现的.
1.4 MESI(缓存一致性)
Modify、Exclusive、Shared、Invalid
, 当CPU写数据时, 如果发现操作的变量是共享变量, 即在其他CPU中也存在该变量的副本, 会发出信号通知其他CPU将该变量的缓存行为置为无效状态, 因此当其他CPU需要读取这个变量时, 发现自己缓存中缓存的该变量的缓存行是无效的, 那么它就会从内存中重新读取.
1.5 嗅探机制
1、例如在x86处理器下, 将volatile变量修饰的共享变量的Java代码转换成汇编代码, 发现会多了lock修饰.
2、Lock前缀的指令在多核处理器下会引发以下事情.
3、将当前处理器缓存行的数据写回到系统内存
4、这个写回内存的操作将会使其它CPU里缓存了该内存地址的数据无效.
5、原理分析:
为了提高处理速度, 处理器不直接和内存进行通信, 而是先将系统内存的数据读到内部缓存(L1, L2或其它)后再进行操作, 但操作完就不知道何时会写到内存. 如果对声明了volatile的变量进行写操作, JVM会向处理器发送一条lock前缀的指令. 将这个变量所在的缓存行的数据写回到系统内存. 在多处理器下, 为了保证各个处理器缓存是一致的, 就会实现缓存一致性协议, 每个处理器通过嗅探
在总线上传播的数据来检查自己的缓存的值是否过期, 当处理器发现自己缓存行对应的内存地址被修改, 就会将当前处理器缓存行设置成无效状态, 当处理器对这个数据进行修改操作的时候, 会重新匆匆系统内存中把数据读到处理器缓存里.
1.6 volatile两条实现原则
1、Lock前缀指令会引起处理器缓存回写到内存:
Lock前缀指令导致在执行指令期间, 声言处理器的#LOCK信号. 在多处理器环境中, LOCK#信号确保在声言该信号期间, 处理器可以独占任何共享内存. 但是在最近的处理器里, LOCK#信号一般不锁总线, 而是锁缓存, 毕竟锁总线的开销比较大. 对于Intel486和Pentium处理器, 在锁操作时, 总是在总线上声言LOCK#信号. 但在P6和目前的处理器中, 如果访问的内存区域已经缓存在处理器内部, 则不会声言LOCK#信号. 相反, 它会锁定这块内存区域的缓存并回写到内存, 并使用缓存一致性机制来确保修改的原子性, 此操作被称为"缓存锁定", 缓存一致性机制会阻止同时修改由两个以上处理器缓存的内存区域数据.
2、一个处理器的缓存回写到内存会导致其它的缓存无效:
IA-32处理器和Intel64处理器使用MESI控制协议去维护内部缓存和其他处理器缓存的一致性. 在多核处理器系统中进行操作的时候, IA-32和Intel 64处理器能嗅探其他处理器访问系统内存和它们的内部缓存, 处理器使用嗅探技术保证它的内部缓存、系统内存和其他处理器的缓存的数据在总线上保持一致. 通过嗅探一个处理器来检测其他处理器打算写内存地址, 而这个地址当前处于共享状态, 那么正在嗅探的处理器将使它的缓存行无效, 在下次访问相同内存地址时, 强制执行缓存行填充.
1.7 Volatile与原子性的关系
Volatile限定的是从缓存读取时刻的校验.
1.8 原子性
1.9 volatile单例模式
public class Instance {
private static volatile Instance instance;
private Instance() {}
public static Instance getInstance() {
if (instance == null) {
instance = new Instance();
}
}
}
上面代码分成三步原子指令:
1、new指令申请内存;
2、在申请的内存中进行Instance的初始化;
3、将申请的内存地址的引用赋值给instance变量;
虽然volatile可以禁止指令重排序, 让上面三个指令有序执行, 但是问题是volatile并不能保证原子性, 所以上面代码中可能出现的问题是当Thread-A执行到第二步进行new Instance初始化时, 此时还没有将地址值赋给instance变量, 所以Thread-B此时看到的instance==null再次进入if中执行new Instance()操作.
所以假设上面代码被两个线程执行, new Instance()会执行两次
参考文章:
https://blog.csdn.net/lsunwing/article/details/83154208
http://www.pianshen.com/article/7227186495/
https://blog.csdn.net/weixin_38623001/article/details/79167596