lazy是当前最稳妥、语义最清晰的线程安全单例实现方式,无需手写锁、不用volatile、不依赖静态构造函数隐式时机;static字段+private构造函数因“判空+赋值”非原子操作,多线程下可能创建多个实例;lazy默认executionandpublication模式确保首次.value访问仅执行一次工厂函数且原子发布。

直接说结论:Lazy<t></t> 是当前最稳妥、语义最清晰的线程安全单例实现方式,无需手写锁、不用 volatile、不依赖静态构造函数的隐式时机。其他写法要么不安全,要么难维护,要么在现代 C# 场景中已过时。
为什么 static 字段 + private 构造函数会创建多个实例
常见错误是只写一个静态字段和私有构造函数,以为这就够了:
public class Singleton
{
private static Singleton _instance;
private Singleton() { }
public static Singleton Instance => _instance ??= new Singleton();
}
问题出在 _instance ??= new Singleton() 或等价的 if (_instance == null) _instance = new Singleton(); —— 这两步(读判空 + 写赋值)不是原子操作。两个线程同时看到 _instance == null 为真,就会各自执行 new Singleton()。
- 典型现象:
Singleton.Instance.GetHashCode()在并发调用中返回不同值 - 根本原因:.NET 不保证普通静态字段的初始化临界性,CLR 只对静态构造函数和
Lazy<t></t>的首次取值做线程安全保证 - 别指望编译器或 JIT 自动加锁——它不会
Lazy 怎么用才真正线程安全
Lazy<t></t> 默认就安全,但必须确认两点:
- 构造委托必须是无副作用的,否则首次访问可能因异常导致后续永远失败
- 不要手动指定
LazyThreadSafetyMode.PublicationOnly,这会允许多次构造(只保证最终发布一个),应保留默认的ExecutionAndPublication -
_instance.Value是唯一入口,不能绕过它直接访问底层字段
正确写法:
public sealed class Singleton
{
private static readonly Lazy<singleton> _instance =
new Lazy<singleton>(() => new Singleton());
<pre class="brush:php;toolbar:false;">public static Singleton Instance => _instance.Value;
private Singleton()
{
// 初始化逻辑放这里,仅执行一次
}
}
什么时候该考虑 DI 容器替代手写单例
如果你在 ASP.NET Core、MAUI、或任何支持 IServiceCollection 的宿主环境中,手写 Singleton 类反而增加耦合和测试难度。
-
services.AddSingleton<isingletonservice singletonservice>()</isingletonservice>天然线程安全,且支持构造注入、生命周期管理(如IDisposable)、作用域验证 - 手写单例适合:工具类(
JsonSerializerOptions封装)、配置读取器、日志门面(ILogger<t></t>本身已由 DI 管理,一般不需再套一层单例) - 控制台或 Unity 启动逻辑等无 DI 场景,
Lazy<t></t>仍是首选
最容易被忽略的一点:单例的“唯一性”只在当前 AppDomain 或当前 AssemblyLoadContext 内有效。跨上下文(比如插件热加载、Blazor WebAssembly 多个隔离上下文)时,Lazy<t></t> 会各自初始化一次——这不是 bug,是设计使然,得按场景决定是否需要全局协调。











