Double-Check-Singleton 双重枷锁单例模式
由于懒汉式单例存在线程安全的问题,有可能在多线程并发的情况下new了多个对象,双重枷锁单例模式正好可以解决这个问题。
先来看代码:
class Singleton {
private static volatile Singleton singleton;
private Singleton() {
}
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
在这段代码中,存在两次检查两次加锁,下面我将具体分析一下:
1) 双重枷锁单例模式是对懒汉单例模式的优化,懒汉单例模式是在第一次调用getInstance时才new对象,因此第一次检查就是第一次判断 singleton == null ,它的目的是确保这是第一次调用getInstance方法。
2) 懒汉单例模式线程不安全,有可能会new多个singleton对象,假如现在有三个线程,同时走到第一次判断sinleton是否为null的地方,这时他们三个都看到sinleton为空,所以都会new一个对象,为了确保线程安全,在new的时候得加一次锁,即使用synchronized关键字进行第一次加锁。
3) 第一次加锁之后,可以保证任意时刻只有一个线程能进来,但是这样还有一个问题,假设线程1进去了,线程2,3卡在锁外面,当线程1new完对象后,线程2,3恢复执行的地方在锁的外面,当线程1释放锁之后,线程2,3依然会进入到锁里边,还是会执行创建对象这一步骤,所以在这里要进行第二次判断,这样才能保证,不管有多少线程,只产生一个对象。
4) 第二次加锁,由于线程1在执行初始化操作的时候,线程2想要调用getInstance方法取得一个Sinleton对象。再仔细分析一下这句代码:sinleton = new Sinleton();
这一句代码不是一个原子性操作,它实际上包含了三个步骤:
- 在堆上开辟空间
- sinleton指向堆空间
- 构造方法完成属性初始化
当线程1执行到第二个步骤的时候,sinleton已经不为null,假设此时线程2刚好进入getInstance方法发现sinleton不为null,它直接返回sinleton对象,这时返回的对象属性有可能还没有初始化,所以使用volatile关键字进行第二次加锁,这样可以保证代码执行到return这一步的时候,前面所有的代码已经执行完毕。换句话说,就是volatile关键字禁止指令重排,它能保证return的这个单例对象一定是属性完全初始化之后的单例对象。