单例模式的一种比较好的写法
package com.volvane.JOffer.test; public class DoubleCheckSingleton { private static DoubleCheckSingleton instance; private DoubleCheckSingleton() { } /** * getInstance 进行了两次判空,第一次判空是为了不必要的同步,第二次判空为了在instance 为 null 的情况下创建实例 * 既保证了线程安全且单例对象初始化后调用getInstance又不会进行同步锁判断 * <p> * 优点:资源利用率高,效率高 * 缺点:第一次加载稍慢,由于java处理器允许乱序执行,偶尔会失败 * * @return */ public static DoubleCheckSingleton getInstance() { if (instance == null) { synchronized (DoubleCheckSingleton.class) { /* * 第二次判空,是因为,第一次的时候判空的时候, * 如果又两个线程a,b同时进“if (instance == null) {”后, * 假设线程a先拿到锁,则b等待 ,a创建了个instance对象,释放锁后, * 线程b进入“synchronized (DoubleCheckSingleton.class) {”后,这时候需要判断下, * instance是否为空,不空就不需要创建直接返回就行了 * */ if (instance == null) { instance = new DoubleCheckSingleton(); } } } return instance; } }
其实这样仍然是有问题的,有个概念叫指令重排序
volatile禁止重排优化
volatile关键字另一个作用就是禁止指令重排优化,从而避免多线程环境下程序出现乱序执行的现象,关于指令重排优化前面已详细分析过,这里主要简单说明一下volatile是如何实现禁止指令重排优化的。先了解一个概念,内存屏障(Memory Barrier)。
内存屏障,又称内存栅栏,是一个CPU指令,它的作用有两个,一是保证特定操作的执行顺序,二是保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)。由于编译器和处理器都能执行指令重排优化。如果在指令间插入一条Memory Barrier则会告诉编译器和CPU,不管什么指令都不能和这条Memory Barrier指令重排序,也就是说通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。Memory Barrier的另外一个作用是强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本。总之,volatile变量正是通过内存屏障实现其在内存中的语义,即可见性和禁止重排优化。下面看一个非常典型的禁止重排优化的例子DCL,如下:
/** * Created by zejian on 2017/6/11. * Blog : http://blog.csdn.net/javazejian [原文地址,请尊重原创] */ public class DoubleCheckLock { private static DoubleCheckLock instance; private DoubleCheckLock(){} public static DoubleCheckLock getInstance(){ //第一次检测 if (instance==null){ //同步 synchronized (DoubleCheckLock.class){ if (instance == null){ //多线程环境下可能会出现问题的地方 instance = new DoubleCheckLock(); } } } return instance; } }
上述代码一个经典的单例的双重检测的代码,这段代码在单线程环境下并没有什么问题,但如果在多线程环境下就可以出现线程安全问题。原因在于某一个线程执行到第一次检测,读取到的instance不为null时,instance的引用对象可能没有完成初始化。因为instance = new DoubleCheckLock();
可以分为以下3步完成(伪代码)
memory = allocate(); //1.分配对象内存空间 instance(memory); //2.初始化对象 instance = memory; //3.设置instance指向刚分配的内存地址,此时instance!=null
由于步骤1和步骤2间可能会重排序,如下:
memory = allocate(); //1.分配对象内存空间 instance = memory; //3.设置instance指向刚分配的内存地址,此时instance!=null,但是对象还没有初始化完成! instance(memory); //2.初始化对象
由于步骤2和步骤3不存在数据依赖关系,而且无论重排前还是重排后程序的执行结果在单线程中并没有改变,因此这种重排优化是允许的。但是指令重排只会保证串行语义的执行的一致性(单线程),但并不会关心多线程间的语义一致性。所以当一条线程访问instance不为null时,由于instance实例未必已初始化完成,也就造成了线程安全问题。那么该如何解决呢,很简单,我们使用volatile禁止instance变量被执行指令重排优化即可。
//禁止指令重排优化 private volatile static DoubleCheckLock instance;