C#单例模式有多种实现,推荐Lazy+readonly字段方案,线程安全且支持延迟初始化;static class无法替代,因其不支持接口、继承、泛型约束、IDisposable及按需初始化。

单例模式在 C# 中不是“只有一种写法”,而是有多个变体,各自适用于不同场景;选错实现方式,轻则线程不安全,重则引发内存泄漏或初始化异常。
为什么不能直接用 static class 替代 Singleton?
static class 看似简单,但它无法实现接口、不能被继承、不能作为泛型约束类型参数,更关键的是——它会在程序集加载时立即初始化所有静态字段,哪怕你根本没用到它。而真正的单例应支持延迟初始化(lazy initialization)。
-
static class无法实现IDisposable,资源释放不可控 - 单元测试时难以 mock 或替换依赖
- 若构造逻辑含副作用(如读配置、连数据库),提前触发会拖慢启动速度
C# 最推荐的单例写法:Lazy + readonly 字段
这是 .NET 4.0+ 下线程安全、延迟加载、简洁可靠的首选方案。CLR 保证 Lazy 的初始化是线程安全的,且只执行一次。
public sealed class Logger
{
private static readonly Lazy _instance = new Lazy(() => new Logger());
public static Logger Instance => _instance.Value;
private Logger() { } // 私有构造,防止外部 new}
-
Lazy 默认使用 LazyThreadSafetyMode.ExecutionAndPublication,无需额外加锁
- 构造函数保持
private,杜绝反射绕过(若需更强防护,可在构造中加 if (Interlocked.Increment(ref _initCount) != 1) throw)
- 不建议在
Instance getter 中做复杂逻辑,否则每次访问都可能触发隐式开销
什么时候该用双重检查锁定(Double-Checked Locking)?
仅当你需要在 .NET 3.5 或更早版本运行,或必须控制初始化时机(比如要传参给构造函数),才考虑 DCL。它容易写错,常见坑包括:
- 忘记用
volatile 修饰实例字段,导致其他线程看到未完全构造的对象
- lock 对象不是私有静态字段,造成锁粒度失控
- 在 lock 外部再次判空时,用了非 volatile 字段,引发重排序问题
public sealed class ConfigLoader
{
private static volatile ConfigLoader _instance;
private static readonly object _lock = new object();
public static ConfigLoader Instance
{
get
{
if (_instance == null)
{
lock (_lock)
{
if (_instance == null)
_instance = new ConfigLoader();
}
}
return _instance;
}
}
private ConfigLoader() { }}
Async 初始化的单例怎么处理?
如果构造过程需要异步操作(如从文件/网络加载配置),Lazy 不适用(它不支持 async 工厂)。此时应改用 AsyncLazy(需自行实现或引用 Microsoft.VisualStudio.Threading)。
- 不要在属性 getter 中直接
await —— 属性不能是 async
- 暴露
Task 类型的静态成员(如 InstanceAsync),调用方负责 await
- 注意首次 await 可能阻塞,后续调用返回已完成 Task
真正难的不是写出一个“能跑”的单例,而是判断它是否该存在——大多数时候,你真正需要的是依赖注入容器(如 Microsoft.Extensions.DependencyInjection)管理生命周期,而不是手写单例。










