面试中如果问到单例模式,最典型就是 DCL(双重效验锁)
下面来看一下懒汉模式中的双重效验锁代码:
public class SingletonLazy {
private static volatile SingletonLazy instance = null;
//构造方法设为私有(保证单例)
private SingletonLazy() { };
//需要用的时候才实例
public static SingletonLazy getInstance() {
//使用双重 if 判定, 降低锁竞争的频率
if(instance == null) {
synchronized (instance) {
// 把读和写打包成原子操作
if(instance == null) { // 读
instance = new SingletonLazy(); // 写
}
}
}
return instance;
}
}
对于 " instance = new SingletonLazy() " 这一行代码来说,它是由三条机器指令组成的:
- 创建内存空间
- 在内存空间中初始化 SingletonLazy 对象
- 将内存地址赋值给 instance 对象(执行这一条指令,instance 就不为 null 了)
【正常情况】 如果这三条机器指令是从上往下的顺序执行,那么就不存在问题。
【异常情况】 在不加 volatile 的情况下,编译器/JVM 为了加快程序执行速度,就会做出优化操作,它会调换 2,3 的执行顺序,那么调换顺序后,在多线程情况下就会出现问题,这个问题也叫做指令重排序。
具体是怎么出现问题的 ??
多线程情况下,当线程 1 执行了 1,3 指令时, 还没来得及执行指令 2,此时线程 2 执行到这个代码了,因为线程 1 执行了指令 3,所以此时 instance 不为 null,那么线程 2 在经过第一个 if 条件判断时,就会返回 false,那么后面的代码就都不执行了,直接就返回 instance 对象了。但是此时的 instance 对象并没有完全实例化,那么线程 2 得到的 instance 就是一个不完整的对象,从而导致程序执行出错。这就是为什么单例对象必须使用 volatile 修饰的原因。
volatile 还有一个作用就是防止内存可见性,它可以保证多个线程在操作同一个变量时,始终可以读到最新的数据,至于什么是内存可见性,volatile 如何应用的,请看这篇文章 - 线程安全问题 的 4.2 部分。