c# 单例模式的线程安全实现 c# double check locking

1次阅读

double-check locking 在 c# 中易出错因内存重排序致未初始化对象被访问;正确写法需 volatile + lock + 二次判空;现代推荐 lazy 或静态构造函数

c# 单例模式的线程安全实现 c# double check locking

为什么 double-check locking 在 C# 中容易出错

因为 .NET 内存模型允许指令重排序,instance = new Singleton() 可能被拆解为「分配内存 → 初始化对象 → 赋值引用」三步,而编译器或 CPU 可能将第三步提前。若此时另一个线程进入第一次检查,会拿到一个未初始化完成的 instance,导致 NULLReferenceException 或更隐蔽的异常。

正确写法:volatile + lock + 二次判空

必须用 volatile 修饰静态字段,确保写操作对所有线程立即可见,并禁止相关重排序;lock 保证构造过程串行化;第二次 if (instance == null) 避免重复初始化。

public sealed class Singleton {     private static volatile Singleton instance;     private static readonly object lockObj = new object(); <pre class='brush:php;toolbar:false;'>private Singleton() { }  public static Singleton Instance {     get     {         if (instance == null)         {             lock (lockObj)             {                 if (instance == null)                 {                     instance = new Singleton();                 }             }         }         return instance;     } }

}

现代 C# 更推荐的替代方案

.NET 4+ 提供了 Lazy<t></t>,它内部已实现线程安全的延迟初始化,且默认采用双重检查逻辑(并做了内存屏障优化),代码更简洁、不易出错:

  • Lazy<t></t>Value 属性首次访问时才执行工厂函数
  • 构造函数调用是线程安全的,且只执行一次
  • 无需手动加锁、无需 volatile,也不用担心重排序
public sealed class Singleton {     private static readonly Lazy<Singleton> lazy =         new Lazy<Singleton>(() => new Singleton()); <pre class='brush:php;toolbar:false;'>private Singleton() { }  public static Singleton Instance => lazy.Value;

}

别忽略静态构造函数这个隐式选项

如果单例不需要延迟初始化(即类加载时就可创建),直接用静态构造函数是最轻量、最安全的方式——CLR 保证其只执行一次且线程安全:

public sealed class Singleton {     private static readonly Singleton instance = new Singleton(); <pre class='brush:php;toolbar:false;'>static Singleton() { } // CLR 保证该类型初始化时仅执行一次  private Singleton() { }  public static Singleton Instance => instance;

}

注意:这种写法会在首次访问该类型任意静态成员时触发初始化,不是真正意义上的“懒加载”,但绝大多数场景下够用,且零开销、零风险。

text=ZqhQzanResources