一尘不染

.NET中双重检查锁定中对volatile修饰符的需求

c#

多篇文章说,在.NET中实施双重检查锁定时,您要锁定的字段应应用volatile修饰符。但是为什么呢?考虑以下示例:

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

为什么“锁(syncRoot)”无法实现必要的内存一致性?在“锁定”语句之后读取和写入都是不稳定的,这样就可以实现必要的一致性,这不是真的吗?


阅读 239

收藏
2020-05-19

共1个答案

一尘不染

挥发是不必要的。好吧,**

volatile用于在变量的读写之间创建内存屏障
lock使用时,lock除了将对块的访问限制在一个线程之外,还会在内的块周围创建内存屏障。
内存屏障使之成为可能,因此每个线程都读取变量的最新值(而不是某些寄存器中缓存的本地值),并且编译器不会对语句进行重新排序。volatile不需要使用
*,因为您已经锁定了。

约瑟夫·阿尔巴哈里(Joseph
Albahari)以前所未有的方式
解释了这种东西。

并且一定要查看Jon Skeet的C#实现单例指南。

update
* volatile导致对变量的读取为VolatileReads ,对变量的写入为VolatileWrites,这在x86和CLR上的x64上使用来实现MemoryBarrier。在其他系统上,它们可能更细粒度。

**仅当您在x86和x64处理器上使用CLR时,我的回答才是正确的。在其他内存模型中,例如在Mono(和其他实现),Itanium64和将来的硬件上,
可能 是正确的。这就是乔恩在“陷阱”中的文章中针对双重检查锁定所指的内容。

为了使代码在内存模型较弱的情况下正常工作,可能需要执行以下操作之一(将变量标记为volatile,使用进行读取Thread.VolatileRead或插入对的调用)Thread.MemoryBarrier

据我了解,在CLR(即使是在IA64上),写入也不会重新排序(写入始终具有发布语义)。但是,在IA64上,除非将其标记为易失性,否则可能会将读取重新排序为先于写入。不幸的是,我无权使用IA64硬件,因此我所说的只是猜测。

我还发现这些文章有所帮助:
http://www.codeproject.com/KB/tips/MemoryBarrier.aspx

万斯莫里森的文章(一切链接到这一点,它谈论的双重检查锁定)
克里斯brumme的文章
(所有链接到本)
乔·达菲(Joe Duffy):双重检查锁定的残破变体

luis abreu的有关多线程的系列也很好地概述了这些概念
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

2020-05-19