双重检查锁定模式:是否存在问题?

9
为什么这个模式被认为是有问题的?在我看来,它很好啊?你有什么想法吗?
public static Singleton getInst() {
    if (instace == null) createInst();
    return instace;
}

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

2
你可以通过使用 DI/IOC 容器来避免这个问题,并允许容器控制对象的生命周期,而不是将这样的逻辑嵌入到对象本身中... 这不是一个答案,但值得思考。 - EightyOne Unite
这里在问题中发布的代码是否算作双重检查锁定的示例?锁只被检查了一次。 - matt b
双重检查锁定(Double-Checked Locking)在Java中是有问题的声明,如何解决? - irreputable
1
@matt 我看到这里有两次 if(instance==null) 的判断。 - atamanroman
7个回答

22

乍一看似乎没问题,但这种技巧存在许多微妙的问题,通常应该避免使用。例如,请考虑以下事件序列:

  1. 线程A注意到值未被初始化,因此获取锁并开始初始化该值。
  2. 编译器生成的代码允许在A完成初始化之前将共享变量更新为指向部分构造的对象。
  3. 线程B注意到共享变量已经被初始化(或者看起来是这样),并返回其值。因为线程B认为该值已经被初始化,所以它不会获取锁。如果B在A完成所有初始化之前就使用对象,则程序可能会崩溃。

您可以通过使用 "volatile" 关键字来正确处理单例实例,从而避免这种情况。


2
+1 - 这是唯一一个指出Java中双重检查锁定模式问题的答案!! - helios
1
Volatile仍然存在一些问题,请看看我的答案。 - atamanroman
1
既不是使用volatile也不是懒加载是正确的方法,而是使用静态初始化程序。 - matt b

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(){ ... }
}

你可能也想将这个类声明为final(虽然私有构造函数已经朝着这个方向了)。 - Sean Patrick Floyd
1
+1,双重检查锁定是所有过早微观优化的根源。 - flybywire
对于枚举构造函数,private 是多余的。编译器会将其设为 private。规范中写道:“在枚举声明中,没有访问修饰符的构造函数声明是私有的。”请参见 https://docs.oracle.com/javase/specs/jls/se8/html/jls-8.html#jls-8.9.2。 - user674669

7
我不确定它是否已经损坏,但由于同步是相当昂贵的,因此这并不是最有效的解决方案。更好的方法是使用“延迟初始化占位符模式”,它会在第一次需要时将单例加载到内存中,正如其名称所示,因此实现了惰性加载。这种模式的最大好处是您无需进行同步,因为JLS确保类加载是串行的。
有关该主题的详细维基百科条目:http://en.wikipedia.org/wiki/Initialization_on_demand_holder_idiom 另一个需要记住的事情是,自从出现了依赖注入框架(例如Spring和Guice),类实例是由这些容器创建和管理的,如果需要,它们将为您提供一个Singleton,因此不值得为此烦恼,除非您想学习模式背后的思想,这是有用的。还要注意,这些IOC容器提供的单例是每个容器实例的单例,但通常您将为每个应用程序使用一个IOC容器,因此这不成问题。

1
仅实例的创建是同步的。 - Jeroen Rosenberg
1
不错的习惯用语。(无论如何问题仍未得到解答)。我应该补充说明,在较新的JVM中,同步的成本较低。 - helios

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

还可以看一下《Effective Java》,其中有一章关于这个主题。

总之,不要使用双重检查锁定,有更好的惯用语。


4

按需初始化占位符模式,就是这样:

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 singleton - the prefered approach
public enum Elvis{
    INSTANCE;
    public void leaveTheBuilding(){ ... }
}

2
大多数回答都正确解释了为什么它出现了问题,但是对于解决方案,不正确或建议可疑。

如果你真的非常需要使用单例模式(在大多数情况下,你不应该这样做,因为它会破坏可测试性,将构造类的逻辑与类的行为组合在一起,向使用单例的类中添加获取单例的知识,并导致更脆弱的代码),并且担心同步问题,正确的解决方案是使用静态初始化器来实例化实例。

private static Singleton instance = createInst();

public static Singleton getInst() {
    return instance ;
}

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

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

2
这并不能回答你的问题(其他人已经回答了),但我想和你分享我们在单例/延迟初始化对象方面的经验:
我们的代码中有一些单例。有一次,我们需要给其中一个单例添加构造函数参数,但是出现了一个严重的问题,因为这个单例的构造函数是在 getter 中被调用的。只有以下几种可能的解决方案:
  • 为需要初始化此单例的对象提供静态getter(或另一个单例)
  • 将初始化单例所需的对象作为getter的参数传递
  • 通过传递实例来摆脱单例
最终,最好的选择是采用最后一个选项。现在,我们在应用程序启动时初始化所有对象,并传递所需的实例(可能作为一个小接口)。我们没有后悔这个决定,因为:
  • 代码的依赖关系非常清晰
  • 我们可以通过提供所需对象的虚拟实现来更轻松地测试我们的代码。

3
单例模式确实被过度使用,往往是设计不良的标志。 - matt b

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接