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

来源:这里教程网 时间:2026-02-21 17:42:27 作者:

为什么 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;

}

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

相关推荐