在Java中使用基于Double Check Locking的Singleton是否安全?

时间:2014-01-27 06:16:44

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

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字段。那么,并发线程将能够读取默认值而不是构造函数中指定的正确值吗?

4 个答案:

答案 0 :(得分:5)

可以在Java 5及更高版本中正确实现双重检查锁定(DCL)。在Java 4及更早版本中,由于volatile关于同步的行为未正确指定(并且在实践中不充分),因此无法实现。

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

但是(IMO),不值得使用DCL。特别是如果您(或开发人员追随您)不完全理解如果正确/安全地如何做到这一点。

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


  

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

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

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

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

答案 1 :(得分:4)

你的引述说:

  

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

说:

  

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

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

另外你说DCL是非常有用的:我会说有更好的方法在几乎所有情况下都不需要使用那个复杂且容易出错的构造。按优先顺序排列:

  • 根本不使用单身人士
  • 使用枚举
  • 使用初始化按需持有人习惯用法

答案 2 :(得分:0)

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

答案 3 :(得分:0)

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

// 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;
    }
}

因为“易失性字段只能访问一次,这可以将方法的整体性能提高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具有较高的初始化成本,并且在正常程序执行期间可能根本不会使用 。但是这种情况通常是一个架构问题而且懒惰初始化只是一个快速的&肮脏的修复。

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