volatile

121 阅读3分钟

懒汉单例引发的问题

1. public class Singleton {
2.     private static volatile Singleton singleton;
3.     private Singleton(){}
4. 
5.     public static Singleton instance(){
6.         if(singleton == null){
7.             synchronized (Singleton.class){
8.                 if(singleton == null){
9.                     singleton = new Singleton();
10.                 }
11.             }
12.         }
13.         return singleton;
14.     }
15. }

我们知道,如果没有volatile,以上代码在多线程环境下会出错。当然还有其他写法保证懒汉单例,在此不多言。 出错的原因是,指令重排。第9行代码在cpu层面是多条指令。

  1. 开辟空间
  2. 初始化对象
  3. 对象地址赋值给变量singleton

经典场景如下

指令重排导致第2,3步顺序对调。 线程T1执行到代码第9行且已完成第3步复制指令,未完成第二步初始化指令。 线程T2执行第6行代码,发现singleton!=null,获取到了未初始化的对象并使用导致运行出错。

解决方案

使用volatile关键字禁止指令重排。

JMM

Java内存模型(Java Memory Model),是jvm的一种规范。 规范了Java虚拟机与计算机内存是如何协同工作的 规定了一个线程如何和何时可以看到由其他线程修改过后的共享变量的值,以及在必须时如何同步的访问共享变量。 用于屏蔽掉各种硬件和操作系统的内存访问差异

volatile语义

先说明,volatile和MESI协议没有的关系。

volatile写/读的内存语义:

  • 线程A写一个volatile变量后,向其他将使用该变量的线程发送变量缓存失效消息。 
  • 线程B读一个volatile变量,如果收到失效消息,则从主存重新读数据。 

内存屏障

由于singleton被volatile修饰,第9行代码编译后,singleton赋值前,虚拟机会添加两个指令,告诉cpu,被这两行各指令包裹的代码不能重排(有序性),还需要从cpu缓存中刷新到主存(可见性)。 这两行指令就是内存屏障。不过各系统内存屏障不同,这里的内存屏障是JVM抽象出来的。

实际上理解了可见性和有序性原因就行了。以下内容其实没啥用。

内存屏障

JVM抽象出来的四个内存屏障

  • storestore
  • storeload
  • loadload
  • loadstore
标题解释
store1;storestore;store2该屏障确保store1立刻刷新数据到内存(使其对其他处理器可见)的操作先于store2及其后所有存储指令的操作
load1;loadload;load2该屏障确保load1数据的装载先于load2及其后所有装载指令的的操作
store1;storeload;store2该屏障确保store1立刻刷新数据到内存的操作先于load2及其后所有装载装载指令的操作。它会使该屏障之前的所有内存访问指令(存储指令和访问指令)完成之后,才执行该屏障之后的内存访问指令
load1;loadstore;store2确保load1的数据装载先于store2及其后所有的存储指令刷新数据到内存的操作

我们注意到以上对内存屏障的解释有"刷新数据到内存"。happens-before实际上是:对主存刷新时机的定义

基于保守策略的JMM内存屏障插入策略:

  • 在每个volatile读操作前,插入一个LoadLoad屏障。 
  • 在每个volatile读操作后,插入一个LoadStore屏障。
  • 在每个volatile写操作前,插入一个StoreStore屏障。 
  • 在每个volatile写操作后,插入一个StoreLoad屏障。 
    如下所示
load指令
store指令

loadload
singleton == null
loadstroe

load指令
store指令

storestore
singleton = new Singleton();
storesload

load指令
store指令