C# NET中双重检查锁定中对volatile修饰符的需要

C# NET中双重检查锁定中对volatile修饰符的需要,c#,singleton,volatile,C#,Singleton,Volatile,多个文本表示,在.NET中实现双重检查锁定时,您锁定的字段应该应用volatile修饰符。但究竟为什么呢?考虑以下例子: public sealed class Singleton { private static volatile Singleton instance; private static object syncRoot = new Object(); private Singleton() {} public static Singleton Insta

多个文本表示,在.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;
      }
   }
}

为什么“lock(syncRoot)”不能实现必要的内存一致性?在“lock”语句之后,读和写都将是易失性的,这样就可以实现必要的一致性了,这不是一篇关于将volatile与双重检查锁定一起使用的非常好的文章吗

在Java中,如果目的是保护一个变量,那么如果它被标记为volatile,则不需要锁定它(注意,我并没有做很多并发的事情)。不。锁只提供多个竞争者(线程)之间的同步

另一方面,volatile告诉您的计算机每次都要重新评估该值,这样您就不会偶然发现缓存的(错误的)值

请参见并注意以下引用:

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


volatile的描述:

volatile是不必要的。嗯,有点**

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

比我能解释的更好

一定要去看看乔恩·斯基特的《C#》


更新:
*
volatile
导致变量读取为
volatireRead
s,写入为
volatireWrite
s,这在CLR上的x86和x64上通过
内存载体实现。在其他系统上,它们可能更细粒度

**只有在x86和x64处理器上使用CLR时,我的答案才是正确的。在其他内存模型中,如Mono(和其他实现)、Itanium64和未来的硬件上,这可能是正确的。这就是Jon在他的文章“gotchas”中提到的双重检查锁定

执行以下操作之一{将变量标记为
volatile
,使用
Thread.volatieread
读取它,或者插入对
Thread.MemoryBarrier
}的调用,可能是代码在弱内存模型情况下正常工作所必需的

据我所知,在CLR上(甚至在IA64上),写操作永远不会重新排序(写操作总是具有发布语义)。但是,在IA64上,读操作可能会被重新排序,以在写操作之前进行,除非它们被标记为volatile。不幸的是,我无法使用IA64硬件,所以我所说的一切都是猜测

我还发现这些文章很有帮助:

(所有内容都链接到此,它谈论双重检查锁定)
(所有内容都链接到此链接)

luis abreu关于多线程的系列文章也很好地概述了这些概念


我认为没有人真正回答了这个问题,所以我将尝试一下

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

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

原因大概是为了避免不必要地执行代码的锁定部分。当您在锁内执行代码时,尝试执行该代码的任何其他线程都会被阻止,如果您尝试频繁地从多个线程访问单实例,则会降低程序的速度。根据语言/平台的不同,您希望避免锁本身的开销

因此,添加了第一个空检查,作为查看是否需要锁的快速方法。如果不需要创建单例,可以完全避免锁

但是如果不以某种方式锁定引用,就无法检查引用是否为null,因为由于处理器缓存,另一个线程可能会更改它,并且您将读取一个“过时”值,这将导致您不必要地输入锁。但你在试图避开锁

因此,您可以将单例设置为volatile,以确保读取最新的值,而无需使用锁

您仍然需要内部锁,因为volatile只在一次访问变量期间保护您-如果不使用锁,您无法安全地测试和设置它

现在,这真的有用吗

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

如果Singleton.Instance可能由于锁而导致效率低下,那么为什么频繁调用它,这将是一个重大问题?单例的全部意义在于只有一个,因此您的代码可以读取和缓存单例引用一次

我能想到的唯一不可能使用这种缓存的情况是当您有大量线程时(例如,使用新线程处理每个请求的服务器可能会创建数百万个运行时间很短的线程,每个线程都必须调用Singleton.Instance一次)


因此,我怀疑双重检查锁定是一种机制,它在非常特定的性能关键情况下有着实际的地位,然后每个人都坚持认为“这是正确的方法”随波逐流,而不实际思考它的作用,以及在他们使用它的情况下是否确实需要它。

锁就足够了。MS语言规范(3.0)本身在§8.12中提到了这一情景,但没有提到
volatile

一个更好的方法是t
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) {
          ...
        }
    }
}
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;
      }
   }
}
private int _value;
public int Value { get { return this._value; } }

private Singleton()
{
    this._value = 1;
}
instance = new Singleton();
ptr = allocate memory for Singleton;
set ptr._value to 1;
set Singleton.instance to ptr;
ptr = allocate memory for Singleton;
set Singleton.instance to ptr;
set ptr._value to 1;
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
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;