在.NET中双重检查locking需要volatile修饰符

多个文本说,当在.NET中执行双重检查locking时,您locking的字段应该应用挥发性修饰符。 但为什么呢? 考虑下面的例子:

public sealed class Singleton { private static volatile Singleton instance; private static object syncRoot = new Object(); private Singleton() {} public static Singleton Instance { get { if (instance == null) { lock (syncRoot) { if (instance == null) instance = new Singleton(); } } return instance; } } } 

为什么不“locking(syncRoot)”完成必要的内存一致性? 在“locking”语句之后,读写是否是不稳定的,这样才能完成必要的一致性?

挥发性是不必要的。 那么,有点**

volatile用于在读取和写入variables之间创build内存屏障*。
在使用lock时,会在lock内部的块周围创build内存障碍,除了限制对一个线程的访问。
内存障碍使得每个线程读取variables的最新值(不是caching在某个寄存器中的本地值),并且编译器不重新sorting语句。 使用volatile是不必要的**,因为你已经有一个锁。

约瑟夫·阿尔巴哈里(Joseph Albahari)解释说,

并且一定要检查Jon Skeet的指南在C#中实现单例

更新
* volatile导致读取的variables为VolatileRead并写入为VolatileWrite ,在CLR上的x86和x64上,使用MemoryBarrier实现。 他们可能在其他系统上更细粒度。

**我的回答只有在x86和x64处理器上使用CLR时才是正确的。 在其他内存模型中,例如Mono(以及其他实现),Itanium64以及未来的硬件可能都是如此。 这是Jon在他的文章中提到的双重lockinglocking问题。

做一个{标记为volatile的variables,用Thread.VolatileRead读取它,或者向Thread.MemoryBarrier插入一个调用}可能是代码在弱内存模型情况下正常工作所必需的。

据我所知,在CLR上(甚至在IA64上),写操作从来没有重新sorting(写操作总是有释放语义)。 但是,在IA64上,读取操作可能需要重新sorting才能写入,除非它们标记为易失性。 不幸的是,我无法使用IA64硬件来玩,所以我所说的任何话都是猜测。

我也发现这些文章有帮助:
http://www.codeproject.com/KB/tips/MemoryBarrier.aspx
vance morrison的文章 (所有链接到这,它谈论双重检查的locking)
克里斯brumme的文章 (一切链接到此)
乔·达菲:双重locking的破碎变体

luis abreu的multithreading系列也给出了一个很好的概念概念
http://msmvps.com/blogs/luisabreu/archive/2009/06/29/multithreading-load-and-store-reordering.aspx
http://msmvps.com/blogs/luisabreu/archive/2009/07/03/multithreading-introducing-memory-fences.aspx

有一种方法来实现它没有volatile领域。 我会解释

我认为这是在内部存储器访问重新sorting是危险的,这样你可以得到一个不完全初始化的实例在锁之外。 为了避免这一点,我这样做:

 public sealed class Singleton { private static Singleton instance; private static object syncRoot = new Object(); private Singleton() {} public static Singleton Instance { get { // very fast test, without implicit memory barriers or locks if (instance == null) { lock (syncRoot) { if (instance == null) { var temp = new Singleton(); // ensures that the instance is well initialized, // and only then, it assigns the static variable. System.Threading.Thread.MemoryBarrier(); instance = temp; } } } return instance; } } } 

了解代码

想象一下,Singleton类的构造函数中有一些初始化代码。 如果这些指令在新的对象的地址被设置后重新sorting,那么你有一个不完整的实例…想象这个类有这样的代码:

 private int _value; public int Value { get { return this._value; } } private Singleton() { this._value = 1; } 

现在想象一下使用new运算符来调用构造函数:

 instance = new Singleton(); 

这可以扩展到这些操作:

 ptr = allocate memory for Singleton; set ptr._value to 1; set Singleton.instance to ptr; 

如果我按这样的顺序重新sorting,

 ptr = allocate memory for Singleton; set Singleton.instance to ptr; set ptr._value to 1; 

这有什么不同吗? 不,如果你想到一个单一的线程。 是的,如果你想到多个线程…如果线程是在set instance to ptr后中断的话:

 ptr = allocate memory for Singleton; set Singleton.instance to ptr; -- thread interruped here, this can happen inside a lock -- set ptr._value to 1; -- Singleton.instance is not completelly initialized 

这就是内存屏障避免,通过不允许内存访问重新sorting:

 ptr = allocate memory for Singleton; set temp to ptr; // temp is a local variable (that is important) set ptr._value to 1; -- memory barrier... cannot reorder writes after this point, or reads before it -- -- Singleton.instance is still null -- set Singleton.instance to temp; 

快乐的编码!

我不认为有人回答这个问题 ,所以我会试一试。

volatile和第一个if (instance == null)不是“必需的”。 锁将使这个代码线程安全。

所以问题是:为什么要添加第一个if (instance == null)

原因可能是为了避免不必要地执行代码的locking部分。 当您在锁内执行代码时,任何其他尝试执行该代码的线程都将被阻塞,这会在您尝试从多个线程频繁访问单例时降低程序运行速度。 根据语言/平台的不同,locking本身也可能会有开销,这是你想避免的。

因此,第一个空检查被添加为一个非常快速的方式,看看你是否需要锁。 如果你不需要创build单例,你可以完全避免锁。

但是你不能检查引用是否为null,而不以某种方式locking它,因为由于处理器caching,另一个线程可能会改变它,并且你会读取一个“stale”值,导致你不必要地input锁。 但是你试图避免locking!

所以你让单体变成volatile,以确保你读取最新的值,而不需要使用锁。

您仍然需要内部锁,因为volatile只在单次访问variables时保护您 – 您无法安全地testing并设置它,而无需使用锁。

现在,这实际上有用吗?

那么我会说“在大多数情况下,不”。

如果Singleton.Instance可能由于locking而导致效率低下,那么为什么如此频繁地调用它,这将是一个重大的问题 ? 单例的全部内容是只有一个,所以你的代码可以读取和caching一次单引用。

唯一的情况是我可以想到这种caching不可能的地方是当你有大量的线程(例如一个服务器使用一个新的线程来处理每个请求可能会创build数百万非常短的线程,每个这将不得不打电话给Singleton.Instance一次)。

所以我怀疑,双重检查locking是一个在非常具体的性能关键的情况下是真正的地方的机制,然后每个人都已经爬上了“这是做这件事的正确方式”的大stream行,而没有真正想到它是什么,在他们使用的情况下实际上是必要的。

AFAIK(和 – 谨慎采取这一点,我没有做很多并发的东西)不。 锁只是让你在多个竞争者(线程)之间同步。

另一方面,volatile会告诉你的机器每次都要重新评估这个值,这样就不会偶然发现一个caching的(错误的)值。

请参阅http://msdn.microsoft.com/en-us/library/ms998558.aspx并注意以下引号:;

此外,该variables被声明为volatile,以确保在实例variables可以被访问之前完成对实例variables的赋值。

volatile的描述: http : //msdn.microsoft.com/en-us/library/x13ttww7%28VS.71%29.aspx

您应该使用双重检查locking模式的易失性。

大多数人指出这篇文章作为certificate你不需要volatile: https : //msdn.microsoft.com/en-us/magazine/cc163715.aspx#S10

但是他们没有读完:“ 最后一个警告 – 我只是在现有的处理器上观察x86内存模型,因此低锁技术也是脆弱的,因为硬件和编译器随着时间的推移会变得更加积极(…)最后,假设最弱的内存模型是可能的,使用volatile声明而不是依赖隐式保证“。

如果您需要更多的说服力,那么请阅读ECMA规范中的这篇文章将用于其他平台:msdn.microsoft.com/en-us/magazine/jj863136.aspx

如果您需要进一步的说服力,请阅读这篇较新的文章,以便优化可以放在不阻碍工作的情况下运行:msdn.microsoft.com/en-us/magazine/jj883956.aspx

总而言之,“可能”对你来说暂时不起作用,但不要写出正确的代码,并且要么使用volatile或者volatile / write方法。 那些build议做的文章有时会遗漏掉可能影响你的代码的JIT /编译器优化的一些可能的风险,以及可能发生的可能会破坏你的代码的未来优化。 另外正如前面提到的假设前面提到的假设在没有波动的情况下已经不能在ARM上持有。

lock已经足够了。 MS语言规范(3.0)本身在§8.12中提到了这个确切的场景,没有提及volatile

更好的方法是通过locking私有静态对象来同步对静态数据的访问。 例如:

 class Cache { private static object synchronizationObject = new object(); public static void Add(object x) { lock (Cache.synchronizationObject) { ... } } public static void Remove(object x) { lock (Cache.synchronizationObject) { ... } } } 

我想我已经find了我正在寻找的东西。 详细信息在本文中 – http://msdn.microsoft.com/en-us/magazine/cc163715.aspx#S10

总结一下 – 在这种情况下,.NET中的volatile修饰符确实不需要。 然而,在较弱的内存模型中,在写入该字段之后,在延迟启动的对象的构造函数中所做的写操作可能会被延迟,因此其他线程可能会在第一个if语句中读取损坏的非空实例。

这是一个相当不错的post关于使用volatile双重检查locking:

http://tech.puredanger.com/2007/06/15/double-checked-locking/

在Java中,如果目标是保护一个variables,如果标记为volatile,则不需要locking

Interesting Posts