设计模式 | 挑战单例模式(三)
四、双重检查锁
加锁的懒汉式的单例最大的缺陷是,每次我们想要获得单例实例时,我们都需要获得一个可能不必要的锁。
为了解决这个问题,我们可以首先从验证是否需要创建对象开始,并且只有在这种情况下我们才会去获得锁。
public class SingletonInDoubleCheckLock {
private volatile static SingletonInDoubleCheckLock INSTANCE = null;
private SingletonInDoubleCheckLock(){}
public static SingletonInDoubleCheckLock getInstance(){
if(INSTANCE == null){
synchronized (SingletonInDoubleCheckLock.class){
if(INSTANCE == null){
INSTANCE = new SingletonInDoubleCheckLock();
}
}
}
return INSTANCE;
}
}
同样的,我们也有两个线程去并发的调用getInstance
方法。
- 任意一个线程进入方法后,先检查变量是否被初始化(不去获得锁),如果已被实例化则立即返回。
- 如果对象为实例化,那就再检查能否获取锁。
- 再次检查变量是否已经被实例化,如果还没被初始化就初始化一个对象。
使用双重检查,非常巧妙地兼顾效率和保证线程安全。
- 第一道检查,可以保证对象如果已经实例化,对于进入到第一道检查的线程,直接获取实例化的对象返回。不再抢占获取锁资源。
- 第二道检查,可以保证对象如果已经被实例化, 刚刚获取到锁资源的线程,直接获取实例化的对象返回,不再实例化新的对象。
但能保证两道检查能够顺利按照预期执行的前提是,使用了volatile
关键字修饰INSTANCE
变量,重排序被禁止,所有的写(write)操作都将发生在读(read)操作之前,保证了实例化的对象对所有用到INSTANCE
的地方可见。
双重检查锁还有一个优点是,它控制了加锁的粒度,懒汉式的加锁方式是对整个静态方法加锁,如果发生阻塞就是基于整个类的阻塞,而双重检查锁是在方法内部的阻塞,可以在逻辑层面避免锁的抢夺。
双重检查锁也有明显的缺陷:
- 要用到
volatile
关键字保证可见性,这个关键字要用在Java1.4
以上的版本。 - 还有就是从代码整洁度和阅读性来说,确实很冗长,可阅读性差。
- 另外总归是要上锁,对程序性能依然存在一定的影响。
我们能采用更好的方法,那就是静态内部类。
转载自:https://juejin.cn/post/7000541505374912543