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










