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


77

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使用不会 违反使用双重检查锁定(即性能)的目的吗?


6
我以为双重检查锁定已损坏,有人修复了吗?
David Heffernan

4
就其价值而言,我发现Head First设计模式是一本可怕的书,值得学习。当我回顾它时,既然我已经在其他地方学习过模式,这是非常有意义的,但是在不了解模式的情况下进行学习实际上并不符合其目的。但这很受欢迎,所以也许只是我很稠密。:-)
corsiKa 2011年

@DavidHeffernan我已经看到了这个示例,它是可以信任jvm进行DCL的一种方式。
内森·费格

FWIW,在x86系统上,易失性Read-Read应该导致无操作。实际上,唯一需要围墙以确保内存一致性的操作就是易失性的读写操作。因此,如果您实际上只将值写入一次,那么影响应该最小。我还没有看到任何人对此进行基准测试,并认为结果会很有趣!
Tim Bender

出于所有实际原因,@ DavidHeffernan仍然存在问题。最好这样做volatile(例如,“不会破坏您的程序”),但那样的话,您赢的并不多。
阿尔夫

Answers:


70

JCIP书籍volatile提供了一个很好的资源,可以理解为什么需要这样做。维基百科也对该材料进行了不错的解释

真正的问题是Thread A可能instance在完成构造之前为其分配存储空间instanceThread B将看到该作业并尝试使用它。Thread B由于使用的是的部分构造版本,因此导致失败instance


1
好的,它看起来像是volatile的新实现,解决了DCL的内存问题。我仍然没有得到的是这里使用volatile的性能含义。从我读到的内容来看,volatile与同步几乎一样慢,那么为什么不同步整个getInstance()方法调用呢?
toc777

5
@ toc777volatile比通常提交的要慢。如果您需要性能,请选择持有人级别的模式。volatile这仅仅是为了表明有一种方法可以使折断的图案起作用。与其说是实际问题,不如说是编码挑战。
阿尔夫

6
@Tim好,XML中的单例仍然是单例;通过使用DI来了解应用程序的运行时状态并不容易。较小的代码单元似乎更简单,但要以强制构造所有单元以符合DI惯用语为代价(有人可能会说这是一件好事)。针对singleton的指控是不公平的,它将API与impl混淆了-Foo.getInstance()只是某种方式使Foo成为表达式,这与@Inject Foo foo; 无论哪种方式,请求Foo的站点都不知道返回哪个Foo,以及静态和运行时依赖性如何相同。
无可争议的2011年

2
@irreputable您知道有趣的是,在我们进行这次交流时,我从未使用过Spring,也没有在指代Spring的动态DI。将Singleton作为a的真正危险static factory是将其称为深入内部代码的诱惑,这种内部代码不应了解该getInstance()方法,而是要求提供实例。
Tim Bender

1
The real problem is that Thread A may assign a memory space for instance before it is finished constructing instance. 那么如何volatile 解决呢?
shaoyihe

21

正如@irreputable所引用的那样,volatile并不昂贵。即使价格昂贵,一致性也应优先于性能。

懒惰单身人士还有另一种干净优雅的方式。

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

来源文章:来自Wikipedia的Initial-on-demand_holder_idiom

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

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

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

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

该解决方案是线程安全的,不需要特殊的语言构造(即volatilesynchronized)。


11

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

撇开情绪,这volatile是因为在第二个线程通过时没有它instance == null,第一个线程可能尚未构建new Singleton():没有人保证对象的创​​建会发生在分配给instance任何线程之前,而实际上是创建对象。

volatile依次建立读写之间的先发生后关系,并修复损坏的模式。

如果您正在寻找性能,请改为使用Holder内部静态类。


1
嗨@alf,根据事前定义,在一个线程释放锁之后,另一个线程获得了锁,然后后者可以看到先前的更改。如果是这样,我认为不需要volatile关键字。你能否更详细地解释它
秦董亮

但是,如果第二个线程仅在外部命中外部,则没有锁获取,因此没有排序。
阿尔夫

@alf,您好,您是否试图指出,当第一个线程在同步块内创建实例时,该实例对于第二个线程可能仍然为null,因为高速缓存未命中,如果实例不是易失性,它将再次实例化?你能澄清一下吗?
Aarish Ramesh

1
@AarishRamesh,不为null;任何状态。有两种操作:为instance变量分配地址,以及在该地址实际创建对象。除非有某种强制执行同步的操作(例如volatile访问或显式同步),否则第二个线程可能会使这两个事件混乱。
ALF

2

如果没有,则在第一个将同步块设置为null后,第二个线程可能会进入同步块,并且本地缓存仍会认为它为null。

第一个不是出于正确性(如果您是对的,那将是自我挫败),而是出于优化目的。


1
根据事前定义,在一个线程释放锁之后,另一个线程获得了锁,然后后者可以看到先前的更改。如果是这样,我认为不需要volatile关键字。你能否更详细地解释它
秦董亮

@QinDongLiang如果变量不是volatile,则第二个线程可能正在其自己的堆栈上使用缓存的值。不稳定会迫使它回到源头以获取适当的值。当然,它必须在每次访问时都这样做,因此可能会影响性能(但是,老实说,除非它处于超临界循环中,否则可能不是系统中最糟糕的事情……)
corsiKa

2

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

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


4
-1我今天失去了声誉:)真正的原因是,存在内存缓存,其建模为线程的本地内存。本地存储器刷新到主存储器的顺序是不确定的-也就是说,除非您发生先于关系,例如使用volatile。寄存器与不完整的对象和DCL问题无关。
阿尔夫

3
您对的定义volatile太狭窄-如果全部都是volatile,那么双重检查锁定在<Java5中就可以正常工作。volatile引入了一个内存屏障,使得某些重新排序是非法的-即使没有从内存中读取过时的值的情况,它仍然是不安全的。编辑:阿尔弗击败了我,不应该给自己喝点好茶;)
Voo

1
@TimBender如果单例包含可变状态,则刷新它与单例本身的引用无关(嗯,有一个间接链接,因为访问volatlie单例的引用会使您的线程重新读取主内存,但这是次要的作用,而不是一个问题:)的原因)
ALF

@alf,您是对的。实际上,如果内部状态可变,则使实例易失性无济于事,因为刷新仅在引用本身发生更改时才会发生(例如使数组/列表易失性不会对内容产生任何影响)。将其粉化成脑屁。
蒂姆·本德尔

根据事前定义,在一个线程释放锁之后,另一个线程获得了锁,然后后者可以看到先前的更改。如果是这样,我认为不需要volatile关键字。您能详细解释吗@Tim Bender
Qin Dong Liang

1

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

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

另一个暗示是通过使用final字段(请参阅维基百科)。这需要额外的读取,这可能会比volatile版本贵。该final版本可能在一个紧密的循环中更快,但是该测试尚无定论。


0

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

请注意

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

缺点

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

单例模式有几种实现,每种都有优点和缺点。

  • 渴望加载单身人士
  • 双重检查锁定单身人士
  • 按需初始化持有人惯用语
  • 基于枚举的单例

详细说明每个都太冗长,因此我只链接了一篇好文章-您想了解的有关Singleton的全部信息

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.