为啥在双重检查锁定中使用volatile
Posted
技术标签:
【中文标题】为啥在双重检查锁定中使用volatile【英文标题】:Why is volatile used in double checked locking为什么在双重检查锁定中使用volatile 【发布时间】:2011-12-12 22:21:18 【问题描述】:从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
的使用不会破坏使用双重检查锁定的目的,即性能吗?
【问题讨论】:
我以为双重检查锁定被破坏了,有人修复了吗? 对于它的价值,我发现 Head First 设计模式是一本可怕的学习书。当我回顾它时,现在我已经在其他地方学习了模式,这是完全有道理的,但是在不知道模式的情况下学习它真的没有达到它的目的。但它非常受欢迎,所以也许只是我太密集了。 :-) @DavidHeffernan 我已经看到这个示例被用作可以信任 jvm 执行 DCL 的一种方式。 FWIW,在 x86 系统上,易失性读取应该导致无操作。事实上,唯一需要为内存一致性设置栅栏的操作是 volatile Write-Read。所以如果你真的只写一次值,那么影响应该很小。我还没有看到有人真正对此进行基准测试并认为结果会很有趣! 查看此链接了解为什么在单例中使用volatile
:cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html
【参考方案1】:
了解为什么需要volatile
的一个很好的资源来自JCIP 书。***也有该材料的decent explanation。
真正的问题是Thread A
可能会在instance
完成构造instance
之前为其分配内存空间。 Thread B
将看到该分配并尝试使用它。这会导致Thread B
失败,因为它使用的是instance
的部分构造版本。
【讨论】:
好的,看起来 volatile 的新实现解决了 DCL 的内存问题。我仍然没有得到的是在这里使用 volatile 对性能的影响。从我读过的内容来看,volatile 和 synchronized 一样慢,那么为什么不同步整个 getInstance() 方法调用呢? @toc777volatile
比通常的归档慢。如果您寻求性能,请选择持有人级模式。 volatile
在这里只是为了表明 有一种方法 可以使损坏的模式起作用。这更像是一个编码挑战,而不是一个真正的问题。
@Tim 好吧,XML 中的单例仍然是单例;使用 DI 并不容易理解应用程序的运行时状态。较小的代码单元可能看起来更简单,代价是强制构建所有单元以符合 DI 习惯用法(有些人可能会说这是一件好事)。对单例的指责是不公平的,它把 API 和 impl 混淆了——Foo.getInstance()
只是一个获取 Foo 的表达式,它和@Inject Foo foo
没有什么不同;无论哪种方式,请求 Foo 的站点都不知道返回哪个 Foo 以及如何返回,无论哪种方式,静态和运行时依赖项都是相同的。
@irreputable 你知道有趣的是,在我们进行这次交流时,我从未使用过 Spring,也没有提到 Spring 不稳定的 DI。将 Singleton 用作 static
factory
的真正危险在于将其称为深层代码的诱惑,而这些代码不应该知道 getInstance()
方法,而是要求提供一个实例。
The real problem is that Thread A may assign a memory space for instance before it is finished constructing instance.
那么volatile
是如何解决这个问题的呢?【参考方案2】:
正如@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之前不会初始化。
静态类LazyHolder
仅在类Singleton上调用静态方法getInstance
时执行,第一次发生这种情况时,JVM将加载并初始化LazyHolder
类。
此解决方案是线程安全的,不需要特殊的语言结构(即volatile
或synchronized
)。
【讨论】:
【参考方案3】:嗯,没有双重检查锁定性能。这是一个破碎的模式。
抛开情绪不谈,volatile
就在这里,因为如果没有它,当第二个线程通过 instance == null
时,第一个线程可能还不能构造 new Singleton()
:没有人保证对象的创建 发生之前 为任何线程分配instance
,但实际创建对象的线程除外。
volatile
反过来在读写之间建立 happens-before 关系,并修复损坏的模式。
如果您正在寻找性能,请改用 holder 内部静态类。
【讨论】:
hi @alf,根据happens-before的定义,一个线程释放锁后,另一个线程获取锁,那么后者可以看到之前的变化。如果是这样,我认为不需要 volatile 关键字。能详细解释一下吗 但是在第二个线程只命中外部if的情况下没有锁获取,所以没有排序。 嗨@alf,那么您是否试图说明当第一个线程在同步块内创建实例时,该实例对于第二个线程可能仍然为空,因为缓存未命中,如果它再次实例化它实例不是易变的?你能澄清一下吗? @AarishRamesh,不为空;任何状态。有两个操作:将地址分配给instance
变量,以及在该地址实际创建对象。除非有强制同步的东西,比如volatile
访问或显式同步,否则第二个线程可能会使这两个事件发生故障。【参考方案4】:
将变量声明为volatile
可以保证对它的所有访问实际上都是从内存中读取其当前值。
如果没有volatile
,编译器可能会优化对变量的内存访问(例如将其值保存在寄存器中),因此只有第一次使用变量时才会读取保存变量的实际内存位置。如果变量在第一次和第二次访问之间被另一个线程修改,这是一个问题;第一个线程只有第一个(修改前)值的副本,因此第二个if
语句测试变量值的旧副本。
【讨论】:
-1 我今天失去了我的声望点 :) 真正的 原因是,有内存缓存,建模为线程的本地内存。本地内存刷新到主内存的顺序是未定义的——也就是说,除非你有 happens-before 关系,例如使用volatile
。寄存器与不完整的对象和DCL问题无关。
您对volatile
的定义太窄了——如果这都是 volatile 所做的,那么双重检查锁定在 volatlie
对单例的引用会使你的线程重新读取主内存——但这是次要影响,而不是问题的原因:))
@alf,你是对的。如果内部状态是可变的,那么实际上使实例 volatile 没有帮助,因为只有在引用本身发生更改时才会发生刷新(例如使数组/列表 volatile 对内容没有任何作用)。把它归结为脑放屁。
根据happens-before的定义,一个线程释放锁后,另一个线程获取锁,那么后者可以看到之前的变化。如果是这样,我认为不需要 volatile 关键字。你能更详细地解释一下吗@Tim Bender【参考方案5】:
如果你没有它,第二个线程可以在第一个设置为 null 之后进入同步块,你的本地缓存仍然会认为它是 null。
第一个不是为了正确(如果你是正确的,那就是自我挫败),而是为了优化。
【讨论】:
根据happens-before的定义,一个线程释放锁后,另一个线程获取锁,那么后者可以看到之前的变化。如果是这样,我认为不需要 volatile 关键字。能详细解释一下吗 @QinDongLiang 如果变量不是易失性的,则第二个线程可能正在使用自己堆栈上的缓存值。不稳定会迫使它回到源头以获得适当的价值。当然,它必须在每次访问时都这样做,因此可能会影响性能(但老实说,除非它处于超级关键循环中,否则它可能不是您系统中最糟糕的事情......)【参考方案6】:易失性读取本身并不昂贵。
您可以设计一个测试以在紧密循环中调用getInstance()
,以观察易失性读取的影响;然而,这种测试是不现实的;在这种情况下,程序员通常会调用一次getInstance()
,并在使用期间缓存实例。
另一个实现是使用final
字段(参见***)。这需要额外的读取,这可能比volatile
版本更昂贵。 final
版本在紧密循环中可能会更快,但如前所述,该测试没有实际意义。
【讨论】:
【参考方案7】:双重检查锁定是一种在多线程环境中调用getInstance
方法时防止创建另一个单例实例的技术。
注意
在初始化之前会检查两次单例实例。 为了提高性能,仅在首次检查单例实例后才使用同步临界区。volatile
实例成员声明中的关键字。这将告诉编译器始终读取和写入主内存而不是 CPU 缓存。使用volatile
变量保证happens-before 关系,所有的写入都将发生在实例变量的任何读取之前。
缺点
由于它需要volatile
关键字才能正常工作,因此它与Java 1.4 及更低版本不兼容。问题是乱序写入可能允许在执行单例构造函数之前返回实例引用。
volatile 变量的拒绝缓存导致性能问题。
在初始化之前会检查两次单例实例。
它非常冗长,使代码难以阅读。
单例模式有多种实现方式,各有优缺点。
渴望加载单例 双重检查锁定单例 Initialization-on-demand 持有者习语 基于枚举的单例每一个的详细描述都太冗长了,所以我只是放了一篇好文章的链接-All you want to know about Singleton
【讨论】:
以上是关于为啥在双重检查锁定中使用volatile的主要内容,如果未能解决你的问题,请参考以下文章
公司发的小师妹夸我好棒,因为我告诉了她项目中用的双重检查锁定是怎么回事