Lazy是当前C#中最稳妥、最推荐的线程安全单例实现方式,它天然支持延迟初始化、无需手写锁、不依赖volatile、语义清晰;其他方案要么不懒、要么易错、要么过时。

直接说结论:Lazy<t></t> 是当前 C# 中最稳妥、最推荐的线程安全单例实现方式,它天然支持延迟初始化、无需手写锁、不依赖 volatile、且语义清晰。其他方案要么不懒(静态构造函数)、要么易错(双重检查锁定)、要么过时(全方法 lock)。
为什么 Lazy<t></t> 能解决绝大多数问题
Lazy<t></t> 内部已封装完整的线程安全逻辑,默认使用 LazyThreadSafetyMode.ExecutionAndPublication,确保构造函数只执行一次,且结果对所有线程可见。它不是“加了锁就安全”,而是从初始化语义层面杜绝竞态。
- 首次访问
_instance.Value才触发构造,真正延迟——哪怕类被引用多次(如读静态常量),实例也不会提前创建 - 构造函数抛异常时,
Lazy<t></t>会缓存该异常,后续再取Value仍抛出同一异常,行为可预测 - 不需要
volatile,也不需要自己管理锁对象,避免手写 DCL 时漏掉内层判空或错用锁粒度 - 性能上:首次访问有微小开销(内部用
Monitor),之后完全无锁,比全程加锁快一个数量级
Lazy<t></t> 的正确写法和常见错误
正确写法必须满足三个条件:字段 static readonly、委托传入构造逻辑、构造函数 private。少一个都可能出问题。
- 必须用
readonly修饰Lazy<t></t>字段,否则字段本身可能被外部改写为新实例 - 构造委托里不能捕获外部变量(尤其是
this或非静态成员),否则可能引发闭包泄漏或线程上下文混乱 - 不要手动调用
IsValueCreated来做“预检”——它不保证线程安全,且破坏了Lazy<t></t>的原子性语义 - 示例中
private static readonly Lazy<singleton> _instance = new Lazy<singleton>(() => new Singleton());</singleton></singleton>是标准模板,不要改成new Lazy<singleton>(new Singleton())</singleton>(这会立即执行构造)
静态构造函数 vs Lazy<t></t>:什么情况下该选前者
静态构造函数本身线程安全,但它是“饿汉式”——只要类型被任意成员首次引用(哪怕只是读一个 public const int),实例就已创建。这在以下场景是硬伤:
- 构造函数需加载大配置、连接数据库、初始化 GPU 资源等重型操作,而该单例实际可能根本不会被用到
- 单元测试中无法重置或替换实例(因为类型初始化后不可逆)
- 构造函数抛异常会导致整个类型永久不可用,后续任何访问都抛
TypeInitializationException - 如果你确实需要“类一加载就初始化”,且确认该资源必用、无副作用、无异常风险,才考虑静态构造函数方式
序列化时单例语义容易被破坏
默认 JSON 或 BinaryFormatter 反序列化会绕过私有构造函数,直接生成新对象,导致单例失效。这不是线程安全问题,但常被忽略。
- 加
[Serializable]后,必须配合[OnDeserializing]方法,在反序列化前把字段设回Instance - 更可靠的做法是:不序列化单例本身,而是序列化其状态数据;或让单例实现
ISerializable,在反序列化构造函数中返回Instance - 若用
System.Text.Json,需自定义JsonConverter<Singleton>,重写Read方法始终返回Instance
真正难处理的从来不是“怎么写一个单例”,而是“怎么确保它在所有边界场景下(高并发、异常、序列化、热重载、DI 替换)都保持唯一”。Lazy<t></t> 解决了最核心的并发初始化问题,但其他环节仍需主动干预,不能默认它能兜底一切。


















