【问题标题】:Is it safe to use Double Checking Locking based Singleton in Java?在 Java 中使用基于双重检查锁定的单例是否安全?
【发布时间】:2014-01-27 06:16:44
【问题描述】:

Wikipedia 上列出了 Java 中的 Singleton 实现之一:

public class SingletonDemo {
    private static volatile SingletonDemo instance = null;

    private SingletonDemo() {
    }

    public static SingletonDemo getInstance() {
        if (instance == null) {
            synchronized (SingletonDemo.class) {
                if (instance == null) {
                    instance = new SingletonDemo();
                }
            }
        }
        return instance;
    }
}

Java Language Specification 17, paragraph 5表示

当一个对象的构造函数完成时,它被认为是完全初始化的。只有在对象完全初始化后才能看到对该对象的引用的线程可以保证看到该对象的最终字段的正确初始化值。

好的,假设我们的 SingletonDemo 类有非 final 字段。那么,并发线程将能够读取默认值而不是构造函数中指定的正确值?

【问题讨论】:

  • 双重检查锁定的答案不是总是“不要使用双重检查锁定”吗?要么不起作用,要么没有帮助。
  • @user2357112 没错,但这是一个有趣的问题。我认为无论如何都应该回答。
  • 不,DCL 通常非常有用,您应该小心使用它。无论如何,这是一个在许多来源中列出的常见示例。所以我只想确定它是否真的安全。这不是关于 Singleton 或 DCL,它只是关于 Java 内存模型和线程模型。
  • 这与您的问题非常相关,因为它准确地解释了为什么它以前不起作用以及为什么现在起作用。
  • @WolframNyaa 该链接是回答您问题的深入解释。它是基于当前几年没有改变的内存模型编写的。您应该阅读它(直到最后)...

标签: java multithreading thread-safety final java-memory-model


【解决方案1】:

在 Java 5 及更高版本中可以正确实现双重检查锁定 (DCL)。在 Java 4 和更早版本中,这是不可能的,因为 volatile 的同步行为没有正确指定(实际上是不充分的)。

您在问题中包含的代码是 DCL 的正确实现 ...当使用 Java 5 JRE 或更高版本运行时。

但是(IMO),不值得使用 DCL。特别是如果您(或追随您的开发人员)不完全了解如何正确/安全地进行操作。

性能优势太小,无法在实际 Java 应用程序中进行有价值的优化。 (如果是这样,你可能过度使用/误用单例......这会以其他方式咬你!)


好的,假设我们的 SingletonDemo 类有非 final 字段。那么,并发线程将能够读取默认值而不是构造函数中指定的正确值?

(引用的 JLS 文本是关于完全不同的情况。它与 final 字段有关。这里不相关。并且您无法从 @987654324 的行为中推断非 final 字段的行为与同步@字段没有同步。)

您的问题的答案是否定的。您问题中的代码足以保证并发线程不会看到默认值。要了解原因,请阅读以下内容:

  • JLS 第 17.4 节的所有内容,以及
  • Goetz 等人的“Java 并发实践”的最后一章,其中包括关于 DCL 的部分(如果我没记错的话……)

【讨论】:

    【解决方案2】:

    你的报价说:

    如果 final 字段和构造函数完成 THEN 线程可以看到初始化值。

    它确实说:

    如果非最终字段 THEN 线程看不到初始化值。

    在该示例中,volatile 的语义也保证了安全发布。

    您还说 DCL 非常有用:我想说有更好的方法不需要在几乎所有情况下使用那种复杂且容易出错的构造。按优先顺序:

    • 根本不要使用单例
    • 使用枚举
    • 使用按需初始化持有人习语

    【讨论】:

      【解决方案3】:

      并发线程甚至可以看到对象的最终字段处于未初始化状态。从您的代码中删除 volatile ,它可能会发生。在没有同步的情况下,只有创建对象的线程才能保证只有在构造完成初始化后才返回对该对象的引用。

      【讨论】:

      • 好的,但这很明显。对非易失性变量的读/写不受之前发生的约束,因此问题与此无关。
      【解决方案4】:

      引用您的维基百科链接,这将是最正确的双重检查易失性解决方案:

      // Broken under Java 1.4 and earlier semantics for volatile
      class Foo {
          private volatile Helper helper;
          public Helper getHelper() {
              Helper result = helper;
              if (result == null) {
                  synchronized(this) {
                      result = helper;
                      if (result == null) {
                          helper = result = new Helper();
                      }
                  }
              }
              return result;
          }
      }
      

      因为“volatile 字段只被访问一次,可以将方法的整体性能提高多达 25%”。

      您也可以使用Initialization-on-demand holder idiom:

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

      这很有效,因为在类加载期间,LazyHolder INSTANCE 直到实际访问该类时才被初始化,即在 getInstance() 方法期间。

      或者只是停止进行过于复杂的初始化,并使用标准的急切初始化:

      static final Singleton INSTANCE = new Singleton();
      

      只有极少数情况下,延迟初始化确实有帮助。也就是说,如果您的 Singleton 具有很高的初始化成本并且 - 在正常程序执行期间 - 可能根本不会使用。但是这种情况通常是一个架构问题,延迟初始化只是一个快速而肮脏的修复。

      所以您应该使用急切初始化除非您分析了您的应用程序并确定这是一个非常负面的性能问题。但是还有很多其他方法可以在架构级别上解决这个问题,而不是使用延迟初始化。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2013-08-08
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多