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