【问题标题】:Why we need double check lock while designing singleton pattern in Java?为什么我们在 Java 中设计单例模式时需要双重检查锁?
【发布时间】:2019-02-20 06:40:56
【问题描述】:

为什么我们需要在获取锁之前和之后检查 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...
}

【问题讨论】:

    标签: java design-patterns synchronization singleton


    【解决方案1】:

    想象下一个场景:

    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()我们有双重初始化

    【讨论】:

      【解决方案2】:

      您检查null 两次,因为:

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

      【讨论】:

        【解决方案3】:

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

        另一种安全初始化单例实例的方法是静态持有者模式,它的实现方式如下:

        public class DclSingleton {
        
            public static DclSingleton getInstance() {
                return Holder.INSTANCE;
            }
        
            private static class Holder {
                private static final DclSingleton INSTANCE = new DclSingleton();
             }
        }
        

        JVM 已经以线程安全的方式初始化类,因此即使 2 个线程同时访问 getInstance(),JVM 也只会初始化一次 Holder 类,因此您可以进行正确的初始化。

        Holder 类也将被延迟加载,因此只有在第一次引用时才会初始化。例如getInstance() 是第一次调用时

        【讨论】:

        • 虽然它会在实践中发挥作用。 Java 规范中不保证惰性。
        • @Line 你是对的。我记得有 JVM 具有急切的类初始化,但它是在 1.5 规范之前。我能找到的最旧的规范是 1.6,所以我不能确定这个约束是后来添加的还是只是我的记忆在玩我:)