为什么在Java中设计单例模式时需要双重检查锁?

Why we need double check lock while designing singleton pattern in Java?

为什么我们需要在获取锁之前和之后检查null? 有一次,我们获得了锁,没有线程可以拥有锁那么为什么在同步块之前不需要空检查?

public class DclSingleton {
    private static volatile DclSingleton instance;
    public static DclSingleton getInstance() {
        **if (instance == null) {**
            synchronized (DclSingleton .class) {
                **if (instance == null) {**
                    instance = new DclSingleton();
                }
            }
        }
        return instance;
    }

    // private constructor and other methods...
}

想象下一个场景:

  1. 线程 1 检查 instance == null 并发现此条件为真。
  2. 线程 2 检查 instance == null 并发现此条件为真。
  3. 线程 1 获取锁。
  4. 线程 2 尝试获取锁,它已经获取,因此线程 2 等待。
  5. 线程 1 初始化 instance = new DclSingleton()
  6. 线程1释放锁。
  7. 线程 2 获取锁。
  8. 线程 2 初始化 instance = new DclSingleton()我们有双重初始化

您检查 null 两次,因为:

  1. 如果在 DclSingleton.class 上同步之前不检查,那么每次调用都会同步,这可能会很慢(想象一下非常频繁地使用单例实例)。
  2. 如果不检查 null inside synchronized 块,多个线程有可能在没有机会的情况下进行第一次检查锁定对象,您将重新创建实例。

要了解为什么需要双重 null 检查,请查看已经给出的答案。

另一种安全初始化单例实例的方法是static holder pattern,它的实现方式如下:

public class DclSingleton {

    public static DclSingleton getInstance() {
        return Holder.INSTANCE;
    }

    private static class Holder {
        private static final DclSingleton INSTANCE = new DclSingleton();
     }
}

JVM已经以线程安全的方式初始化了classes,所以即使2个线程同时访问getInstance(),JVM也会初始化Holderclass 只有一次,因此你有正确的初始化。

另外Holder class会延迟加载,所以只有在第一次被引用时才会初始化。例如,当 getInstance() 是第一次被调用时