lazy 是最推荐的单例实现方式,它内置线程安全与延迟初始化,避免手动加锁或静态字段初始化的竞态问题,且兼容所有现代 .net 版本。

为什么 static 字段 + 私有构造函数还不够安全
很多人写完 private static readonly MySingleton _instance = new MySingleton(); 就以为稳了,但多线程下可能创建多个实例。.NET 早期版本(如 .NET Framework 4.0 之前)中,static 初始化器的执行时机不保证线程安全——两个线程同时首次访问类时,可能各自触发初始化,导致两次构造。
- 必须显式加锁或依赖
Lazy<t></t>的原子性保障 - .NET Core / .NET 5+ 对静态构造函数做了更强的线程安全保证,但仅限于「静态构造函数」本身;普通
static字段初始化仍不自动同步 - 如果单例要传参、做耗时初始化(比如读配置、连数据库),就不能靠静态字段直接 new
Lazy<t></t> 是最省心也最推荐的方式
它把延迟初始化和线程安全打包解决了,且默认使用 LazyThreadSafetyMode.ExecutionAndPublication,即首次调用 Value 时只执行一次构造,并确保所有线程看到同一个结果。
public sealed class DatabaseService
{
private static readonly Lazy<databaseservice> _instance =
new Lazy<databaseservice>(() => new DatabaseService());
public static DatabaseService Instance => _instance.Value;
private DatabaseService()
{
// 可放心放耗时逻辑,只执行一次
}
}</databaseservice></databaseservice>
-
Lazy<t></t>在 .NET Framework 4.0+ 和所有现代 .NET 版本都可用 - 不要手动 new
Lazy<t></t>后又暴露其对象——必须通过.Value访问,否则绕过懒加载和线程保护 - 如果初始化可能失败(抛异常),
Lazy<t></t>会缓存异常,后续访问直接重抛;这不是 bug,是设计行为
不用锁的手动双重检查锁定(DCL)容易写错
有人为了“性能”手写 lock + 两次判空,但极易漏掉 volatile 或锁粒度不对,导致指令重排或竞态。以下是最简正确写法:
public sealed class CacheManager
{
private static volatile CacheManager _instance;
private static readonly object _lock = new object();
public static CacheManager Instance
{
get
{
if (_instance == null)
{
lock (_lock)
{
if (_instance == null) // 再次检查
_instance = new CacheManager();
}
}
return _instance;
}
}
private CacheManager() { }
}
- 缺
volatile:可能导致其他线程看到未完全构造的对象(尤其在 x86/x64 弱内存模型下) - 锁对象不能是
this或类型本身(typeof(CacheManager)),否则外部代码可能意外参与同一把锁 - DCL 在绝大多数场景下没必要——
Lazy<t></t>性能足够,且更可靠
别在 ASP.NET Core 里自己写单例,交给 DI 容器
如果你的类是服务(比如 IEmailSender),直接注册为 Singleton 即可,框架会帮你管好生命周期和线程安全:
services.AddSingleton<iemailsender smtpemailsender>();</iemailsender>
- DI 容器内部用的是类似
Lazy<t></t>的机制,不是裸 new - 避免手动单例和 DI 单例混用:比如类既实现了自己的
Instance,又被注册进容器,会导致两个实例 - 若需在非 DI 上下文(如工具类、静态方法)中获取,应通过
IServiceProvider解析,而不是硬编码单例访问











