仅用static字段+private构造函数不安全,因多线程下可能同时通过null检查而创建多个实例;应使用Lazy(默认ExecutionAndPublication)或DI容器注册实现真正线程安全单例。

为什么 static 字段 + private 构造函数还不够安全
因为多线程下可能创建多个实例。常见错误是只加 private 构造函数和静态字段,没控制初始化时机,导致两个线程同时判断 instance == null 为真,各自 new 一次。
- 典型现象:
Singleton.Instance.GetHashCode()在高并发测试中返回不同值 - 根本原因:.NET 的静态字段初始化不是原子操作,
if (instance == null) instance = new Singleton();不是临界区 - 别用双重检查锁定(DCL)手写锁——容易漏掉
volatile或锁粒度不对,C# 有更稳的方案
推荐做法:用 Lazy 实现真正线程安全的单例
Lazy 内置线程安全保证,且延迟初始化,语义清晰、无竞态、无额外锁开销。
- 必须指定
LazyThreadSafetyMode.ExecutionAndPublication(默认值),否则可能不安全 - 构造函数仍需
private,防止外部 new - 性能上:首次访问有微小开销(内部用
Monitor),但之后完全无锁,比手写锁更优 - 示例:
public sealed class Singleton { private static readonly Lazy<singleton> _instance = new Lazy<singleton>(() => new Singleton()); public static Singleton Instance => _instance.Value; private Singleton() { } }</singleton></singleton>
如果必须用静态构造函数,要注意什么
静态构造函数本身线程安全,但只能做“立即初始化”,无法延迟加载,且一旦抛异常,类型将永久不可用。
- 适用场景:单例依赖项简单、启动即需、能接受初始化失败即崩
- 不能带参数,也不能捕获异常后重试
- 和
Lazy相比,它会在第一次访问任何静态成员时触发,哪怕你只读了另一个静态字段 - 错误写法:
static Singleton() { instance = new Singleton(); }—— 若构造函数抛异常,后续所有Singleton.Instance都会抛TypeInitializationException
别忽略 .NET Core/.NET 5+ 的生命周期替代方案
在 ASP.NET Core 或支持 DI 容器的场景里,直接手写单例类反而增加维护成本,也难测、难替换。
- 注册方式:
services.AddSingleton(); - 容器管理的单例天然线程安全,且支持构造注入、作用域验证、Dispose 管理
- 手写单例类更适合工具类(如日志器、配置读取器)、或非 DI 环境(如控制台、Unity 启动逻辑)
- 混用风险:DI 容器里注册了单例,又在代码里调
Singleton.Instance,等于两套生命周期,状态可能不一致
实际项目里最常出问题的,不是写法错,而是没想清楚“这个单例到底该由谁负责生命周期”——是自己管,还是交给容器;是必须懒加载,还是可以随类型加载。选错这点,后面补锁、改构造、拆依赖,都得返工。











