为什么在双重检查锁定中使用volatile

时间:2011-10-21 21:53:11

标签: java design-patterns locking singleton double-checked-locking

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

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

7 个答案:

答案 0 :(得分:58)

理解为什么需要volatile的良好资源来自JCIP一书。维基百科也有decent explanation个材料。

真正的问题是Thread A可能会在instance构建instance之前为Thread B分配一个内存空间。 Thread B会看到该分配并尝试使用它。这会导致instance失败,因为它使用的是部分构造版本的{{1}}。

答案 1 :(得分:16)

正如@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变量,因此初始化很简单。

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

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

此解决方案是线程安全的,无需特殊的语言结构(即volatilesynchronized)。

答案 2 :(得分:8)

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

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

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

如果您正在寻找性能,请改用holder inner static class。

答案 3 :(得分:2)

如果你没有它,第二个线程可能会在第一个线程设置为null之后进入synchronized块,而你的本地缓存仍然认为它是null。

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

答案 4 :(得分:1)

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

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

另一个意思是使用final字段(请参阅维基百科)。这需要额外的读取,这可能比volatile版本更昂贵。 final版本在紧密循环中可能更快,但是该测试没有像之前所说的那样有用。

答案 5 :(得分:0)

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

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

答案 6 :(得分:0)

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

注意

  • 单个实例在初始化之前被检查了两次。
  • 出于此原​​因,只有在首先检查单例实例之后才使用同步关键部分。
  • volatile关键字,用于实例成员的声明。这将告诉编译器始终从主内存而不是从CPU高速缓存读取和写入。使用volatile变量保证事前发生关系,所有写操作都将在实例变量的任何读操作之前发生。

缺点

  • 由于它需要volatile关键字才能正常工作,因此它与Java 1.4及更低版本不兼容。问题在于,无序写操作可能允许实例引用在执行单例构造函数之前被返回。
  • 由于易失性变量的缓存减少而导致的性能问题。
  • 单个实例在初始化之前检查了两次。
  • 这很冗长,使代码难以阅读。

单例模式的几种实现各有优缺点。

  • 急于加载单例
  • 双重检查锁定单例
  • 按需初始化持有人习惯用法
  • 基于枚举的单例

每个人的详细说明都太冗长,因此我只链接了一篇好文章-All you want to know about Singleton