【问题标题】:Is there any need to add volatile keyword to guarantee thread-safe singleton class in java?是否需要添加 volatile 关键字来保证 java 中的线程安全单例类?
【发布时间】:2016-04-07 09:42:15
【问题描述】:

根据post,线程安全的单例类应该如下所示。但我想知道是否需要将 volatile 关键字添加到 static CrunchifySingleton instance 变量。由于如果实例被创建并存储在 CPU 缓存中,此时它不会被写回主内存,同时,另一个线程会调用 getInstance() 方法。会不会产生不一致的问题?

public class CrunchifySingleton {

    private static CrunchifySingleton instance = null;

    protected CrunchifySingleton() {
    }

    // Lazy Initialization
    public static CrunchifySingleton getInstance() {
        if (instance == null) {
            synchronized (CrunchifySingleton.class) {
                if (instance == null) {
                    instance = new CrunchifySingleton();
                }
            }
        }
        return instance;
    }
}

【问题讨论】:

  • 懒惰初始化的单例是一种浪费;一般来说,单身人士是个坏主意。谷歌已经编写了软件来识别它们,以便从它们的代码中删除。你为什么要使用它们?

标签: java multithreading singleton volatile


【解决方案1】:

我赞同上面@duffymo 的评论:懒惰的单身人士远没有它们最初看起来那么有用。

但是,如果您绝对必须使用延迟实例化的单例,lazy holder idiom 是实现线程安全的更简单方法:

public final class CrunchifySingleton {
  private static class Holder {
    private static final CrunchifySingleton INSTANCE = new CrunchifySingleton();
  }

  private CrunchifySingleton() {}

  static CrunchifySingleton getInstance() { return Holder.INSTANCE; }
}

另外,请注意,要成为真正的单例,类需要禁止实例化和子类化——构造函数需要分别为private,类需要分别为final

【讨论】:

  • 私有构造函数和最终类并不是真正需要的。私有构造函数是的,或者有人可以实例化。但是,如果您有一个私有构造函数,那么该类无论如何都是隐式最终的。但是将其标记为最终版本当然可以清楚地说明它。 :-)
【解决方案2】:

是的,如果您的 Singleton 实例不是 volatile 或者即使它不是 volatile,但您使用的是足够旧的 JVM,则该行所在的操作没有顺序保证

instance = new CrunchifySingleton();

根据volatile 存储分解。

然后编译器可以重新排序这些操作,以便您的实例不为空(因为已分配内存),但仍未初始化(因为尚未执行其构造函数)。

如果您想了解更多关于双重检查锁定的隐藏问题,特别是在 Java 中,请参阅The "Double-Checked Locking is Broken" Declaration

lazy holder idiom 是一个很好的模式,可以很好地概括一般静态字段延迟加载,但如果您需要一个安全且简单的 Singleton 模式,我会推荐 Josh Bloch(来自 Effective Java成名)推荐 - Java Enum Singleton

public enum Elvis {
    INSTANCE;

    public void leaveTheBuilding() { ... }
}

【讨论】:

    【解决方案3】:

    你引用它的代码在Java中被破坏了。是的,您需要 volatile 和至少 Java 5 才能使双重检查习语线程安全。您还应该在延迟初始化中添加一个局部变量以提高性能。在此处阅读更多信息:https://en.wikipedia.org/wiki/Double-checked_locking#Usage_in_Java

    【讨论】:

    • 我不会在 2016 年指导任何人使用 DCL。目前的标准是 enum singleton
    • 我不只是问他的版本是否是线程安全的,这就是答案。
    • 顺便说一句。枚举单例模式很好,但是如果需要,你不能做延迟初始化,可以吗?
    • 好吧,正如 duffymo 非常正确地提到的那样,延迟初始化几乎是人们认为他们需要的白日梦(即使他们无法给出理由)。然而,枚举单例是“本机”的,在第一次使用时加载实例类(就像持有者模式一样)。
    【解决方案4】:

    是的,设置 volatile 将保证每次任何线程尝试访问您的关键代码部分时,线程都会从内存本身而不是从线程缓存中读取数据。

    【讨论】:

      【解决方案5】:

      在这种情况下您需要volatile,但更好的选择是使用enum(如果它是无状态的)

      enum Singleton {
           INSTANCE;
      }
      

      但是应该尽可能避免有状态的单例。我建议您尝试创建一个通过依赖注入传递的实例。

      【讨论】:

        最近更新 更多