话不多说,直接贴代码
class Singleton { private static volatile Singleton instance; private Singleton(){}
//双重判空 public static Singleton getInstance() { if ( instance == null ) { synchronized (Singleton.class) { if ( instance == null ) { instance = new Singleton(); } } } return instance; } }
这是一个大家耳熟能详的单例实现,其中有两个关键要点,一是使用双重检查锁定(Double-Checked Locking)来尽量延迟加锁时间,以尽量降低同步开销;二就是instance实例上加了volatile关键字。那么为什么一定要加volatile关键字,volatile又为我们做了什么事情呢?
要了解这个问题,我们先要搞清楚三个概念:java内存模型(JMM)、happen-before原则、指令重排序。
1.java内存模型(Java Memory Model)
Java内存模型中规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程的工作内存中使用到的变量需要到主内存去拷贝,线程对变量的所有操作(读取、赋值)都必须在工作内存中进行,而不能直接读写主内存中的变量。不同线程之间无法直接访问对方工作内存中的变量,线程间变量值的传递均需要在主内存来完成,线程、主内存和工作内存的交互关系如下图所示:
2.happen-before原则
Java语言中有一个“先行发生”(happen—before)的规则,它是Java内存模型中定义的两项操作之间的偏序关系,如果操作A先行发生于操作B,其意思就是说,在发生操作B之前,操作A产生的影响都能被操作B观察到,“影响”包括修改了内存中共享变量的值、发送了消息、调用了方法等,它与时间上的先后发生基本没有太大关系。这个原则特别重要,它是判断数据是否存在竞争、线程是否安全的主要依据。
下面是Java内存模型中的八条可保证happen—before的规则,它们无需任何同步器协助就已经存在,可以在编码中直接使用。如果两个操作之间的关系不在此列,并且无法从下列规则推导出来的话,它们就没有顺序性保障,虚拟机可以对它们进行随机地重排序。
- 单线程happen-before原则:在同一个线程中,书写在前面的操作happen-before后面的操作。
- 锁的happen-before原则:同一个锁的unlock操作happen-before此锁的lock操作。
- volatile的happen-before原则:对一个volatile变量的写操作happen-before对此变量的任意操作(当然也包括写操作了)。
- happen-before的传递性原则:如果A操作 happen-before B操作,B操作happen-before C操作,那么A操作happen-before C操作。
- 线程启动的happen-before原则:同一个线程的start方法happen-before此线程的其它方法。
- 线程中断的happen-before原则:对线程interrupt方法的调用happen-before被中断线程的检测到中断发送的代码。
- 线程终结的happen-before原则:线程中的所有操作都happen-before线程的终止检测。
- 对象创建的happen-before原则:一个对象的初始化完成先于他的finalize方法调用。
3.指令重排序
对主存的一次访问一般花费硬件的数百次时钟周期。处理器通过缓存(caching)能够从数量级上降低内存延迟的成本,这些缓存为了性能重新排列待定内存操作的顺序。也就是说,程序的读写操作不一定会按照它要求处理器的顺序执行。
JMM通过happens-before法则保证顺序执行语义,如果想要让执行操作B的线程观察到执行操作A的线程的结果,那么A和B就必须满足happens-before原则,否则,JVM可以对它们进行任意排序以提高程序性能。
基于以上三个概念,我们可以拆解 instance = new Singleton() 这段代码:
// thread-A
memory = allocate(); // 1:分配对象的内存空间 ctorInstance(memory); // 2:初始化对象 instance = memory; // 3:设置instance指向刚分配的内存地址
然而,由于happen-before原则并不能保证这段代码的顺序性,这段代码可能被编译器优化为:
//thread-B
memory = allocate(); // 1:分配对象的内存空间 instance = memory; // 3:设置instance指向刚分配的内存地址 ctorInstance(memory); // 2:初始化对象
在单线程中不论是以哪种顺序执行,都不会对结果有任何影响,然而在多线程下,有可能出现thread-B的执行顺序,尽管由于同步锁的存在,不会出现两个线程同时进入instance = new Singleton()的场景,但是若B线程执行完3之后,2还没有执行,CPU就切换时间片,执行一个全新的C线程,将导致C线程拿到一个非空的instance,然而这时候该instance还没有准备好。
而这一切,仅仅需要在instance实例前加上volatile,就可以完美的解决。
那么,volatile在例子中到底做了什么神奇的操作呢?
其一,对于volatile修饰的instance变量,若对instance的写操作执行在前,那么该写操作的结果一定会被立刻刷新到主内存中,之后所有线程对于该instance的所有读写操作必然可以观察到最新的值,也即:volatile保证了变量的内存可见性
其二,对于volatile修饰的instance变量,将不允许任何与其相关的操作进行指令重排序