双重检查锁定模式:是否破碎?

时间:2010-09-01 09:48:37

标签: java concurrency singleton double-checked-locking

为什么这种模式被认为是破碎的?它看起来很好吗?有什么想法吗?

public static Singleton getInst() {
    if (instace == null) createInst();
    return instace;
}

private static synchronized createInst() {
     if (instace == null) {
         instace = new Singleton(); 
     }
}

7 个答案:

答案 0 :(得分:21)

乍一看看起来不错,但这种技术有许多微妙的问题,通常应该避免。例如,请考虑以下事件序列:

  1. 线程A注意到值为 没有初始化,所以它得到了 锁定并开始初始化 值。
  2. 允许编译器生成的代码 将共享变量更新为 指向部分构造 A完成之前的对象 执行初始化。
  3. 线程B注意到共享 变量已初始化(或左右) 它出现),并返回其值。 因为线程B相信价值 已初始化,但事实并非如此 获得锁。如果B使用 所有的对象 由A完成的初始化可以看到 B程序可能会崩溃。
  4. 您可以通过使用“volatile”关键字正确处理单例实例来避免这种情况

答案 1 :(得分:11)

整个讨论是一个巨大的,无休止的大脑时间浪费。 99.9%的时间,单身人士没有任何重大的设置成本,并且没有任何理由设计设置来实现非同步保证延迟加载。

这就是你在Java中编写Singleton的方法:

public class Singleton{
    private Singleton instance = new Singleton();
    private Singleton(){ ... }
    public Singleton getInstance(){ return instance; }
}

更好的是,让它成为一个枚举:

public enum Singleton{
    INSTANCE;
    private Singleton(){ ... }
}

答案 2 :(得分:7)

我不知道它是否被破坏,但由于同步这种方法相当昂贵,它并不是真正最有效的解决方案。 更好的方法是使用'Initialization On Demand Holder Idiom',它在第一次需要时将你的单例加载到内存中,顾名思义,因此延迟加载。 这个习惯用法带来的最大好处是你不需要同步,因为JLS确保类加载是串行的。

关于该主题的详细维基百科条目:http://en.wikipedia.org/wiki/Initialization_on_demand_holder_idiom

要记住的另一件事是,由于Spring和Guice之类的依赖注入框架已经出现,正在创建类实例并由这些容器创建和管理,如果需要它们将为您提供Singleton,因此它不值得除非你想学习模式背后的想法,这是有用的。 另请注意,这些IOC容器提供的单例是每个容器实例的单例,但通常每个应用程序都有一个IOC容器,因此它不会成为问题。

答案 3 :(得分:6)

问题如下:您的JVM可能会重新排序您的代码,并且字段对于不同的线程并不总是相同。看看这个:http://www.ibm.com/developerworks/java/library/j-dcl.html。使用volatile关键字应该解决这个问题,但是在java 1.5之前它已经破坏了。

大多数时候,单次检查锁定的速度已经足够快,请尝试以下方法:

// single checked locking: working implementation, but slower because it syncs all the time
public static synchronized Singleton getInst() {
    if (instance == null) 
        instance = new Singleton();
    return instance;
}

另请参阅有效的java,在这里您可以找到关于此主题的精彩章节。

总结一下:不要做双重检查锁定,有更好的idoms。

答案 4 :(得分:4)

Initialization On Demand Holder Idiom,是的,就是这样:

public final class SingletonBean{

    public static SingletonBean getInstance(){
        return InstanceHolder.INSTANCE;
    }

    private SingletonBean(){}

    private static final class InstanceHolder{
        public static final SingletonBean INSTANCE = new SingletonBean();
    }

}

虽然Joshua Bloch也在Effective Java第2章第3项中推荐了Enum单身人士模式:

// Enum singleton - the prefered approach
public enum Elvis{
    INSTANCE;
    public void leaveTheBuilding(){ ... }
}

答案 5 :(得分:2)

这不回答你的问题(其他人已经做过),但我想告诉你我对单身/懒惰初始化对象的体验:

我们的代码中有几个单身人士。一旦我们必须将构造函数参数添加到一个单例并且遇到严重问题,因为在getter上调用了这个单例的构造函数。只有以下可能的解决方案:

  • 为初始化此单例所需的对象提供静态getter(或其他单例),
  • 传递对象以初始化单例作为getter的参数或
  • 通过传递实例来摆脱单身人士。

最后,最后一个选择是要走的路。现在我们在应用程序启动时初始化所有对象并传递所需的实例(可能是一个小接口)。我们没有后悔这个决定,因为

  • 一段代码的依赖关系非常明确,
  • 我们可以通过提供所需对象的虚拟实现来更轻松地测试我们的代码。

答案 6 :(得分:2)

这里的大部分答案都是正确的,为什么它被打破,但是不正确或暗示可疑的解决方案策略。

如果你真的那么,真的必须使用单例(在大多数情况下你不应该,因为它破坏了可测试性,结合了如何构建类的逻辑class的行为,使用单例知道如何获取单一的类,并导致更脆弱的代码)并且关注同步,正确的解决方案是使用a static initializer来实例化实例。

private static Singleton instance = createInst();

public static Singleton getInst() {
    return instance ;
}

private static synchronized createInst() {
    return new Singleton(); 
}

Java语言规范保证静态初始化器只运行一次,第一次加载类时,并以保证的线程安全方式运行。