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

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