83

Head First设计模式一书中,具有双重检查锁定的单例模式已实现如下:

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

我不明白为什么volatile被使用。使用是否会破坏 volatile使用双重检查锁定的目的,即性能?

4

7 回答 7

72

JCIP书籍是理解为什么volatile需要的一个很好的资源。维基百科对该材料也有很好的解释

真正的问题是,在它完成构建之前Thread A可能会为其分配一个内存空间。将看到该分配并尝试使用它。这会导致失败,因为它使用的是部分构造的.instanceinstanceThread BThread Binstance

于 2011-10-21T22:01:04.207 回答
25

正如@irreputable 所引用的,易失性并不昂贵。即使代价高昂,一致性也应优先于性能。

Lazy Singletons 还有一种更简洁优雅的方式。

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

来源文章:来自维基百科的 Initialization-on-demand_holder_idiom

在软件工程中,Initialization on Demand Holder(设计模式)习语是延迟加载的单例。在所有 Java 版本中,该习惯用法都可以实现安全、高并发的延迟初始化,并具有良好的性能

由于该类没有任何static要初始化的变量,因此初始化很容易完成。

LazyHolder在 JVM 确定必须执行 LazyHolder 之前,不会初始化其中的静态类定义。

静态类仅在类 Singleton 上调用LazyHolder静态方法时才会执行getInstance,并且第一次发生这种情况时,JVM 将加载并初始化LazyHolder该类。

这个解决方案是线程安全的,不需要特殊的语言结构(即volatileor synchronized)。

于 2016-03-19T08:38:52.610 回答
12

好吧,没有针对性能的双重检查锁定。这是一个破碎的模式。

撇开情绪不谈,volatile是因为在第二个线程通过时没有它instance == null,第一个线程可能还没有构造new Singleton():没有人承诺对象的创建发生在分配给instance任何线程之前,但实际创建对象的线程。

volatile反过来建立读取和写入之间的发生前关系,并修复损坏的模式。

如果您正在寻找性能,请改用持有人内部静态类。

于 2011-10-21T22:01:07.523 回答
3

将变量声明为volatile保证对它的所有访问实际上都是从内存中读取其当前值。

如果没有volatile,编译器可能会优化对变量的内存访问(例如将其值保存在寄存器中),因此只有第一次使用变量时才会读取保存变量的实际内存位置。如果变量在第一次和第二次访问之间被另一个线程修改,这是一个问题;第一个线程只有第一个(预先修改的)值的副本,因此第二个if语句测试变量值的陈旧副本。

于 2011-10-21T21:58:27.957 回答
2

如果你没有它,第二个线程可以在第一个设置为空之后进入同步块,并且你的本地缓存仍然认为它是空的。

第一个不是为了正确性(如果你是正确的那将是自我挫败),而是为了优化。

于 2011-10-21T21:58:06.407 回答
1

易失性读取本身并不昂贵。

您可以设计一个测试以getInstance()在紧密循环中调用,以观察易失性读取的影响;然而,这种测试是不现实的;在这种情况下,程序员通常会调用getInstance()一次并在使用期间缓存实例。

另一个实现是使用一个final字段(参见维基百科)。这需要额外的读取,这可能会比volatile版本更昂贵。该final版本在紧密循环中可能会更快,但是如前所述,该测试没有实际意义。

于 2011-10-22T03:04:35.853 回答
-2

getInstance双重检查锁定是一种在多线程环境中调用方法时防止创建另一个单例实例的技术。

注意

  • Singleton 实例在初始化之前检查了两次。
  • 同步临界区仅在第一次检查单例实例后才使用,以提高性能。
  • volatile实例成员声明中的关键字。这将告诉编译器始终读取和写入主内存而不是 CPU 缓存。通过volatile变量保证发生前的关系,所有的写入都将发生在实例变量的任何读取之前。

缺点

  • 由于它需要volatile关键字才能正常工作,因此它与 Java 1.4 及更低版本不兼容。问题是乱序写入可能允许在执行单例构造函数之前返回实例引用。
  • 由于 volatile 变量的缓存下降导致的性能问题。
  • Singleton 实例在初始化之前检查了两次。
  • 它非常冗长,并且使代码难以阅读。

单例模式有多种实现方式,各有优缺点。

  • 急切加载单例
  • 双重检查锁定单例
  • 按需初始化持有者成语
  • 基于枚举的单例

每一个的详细描述都太冗长了,所以我只是放了一篇好文章的链接——关于 Singleton 你想知道的一切

于 2020-01-27T10:28:41.093 回答