饿汉式单例在类加载时即创建实例,线程安全但不支持懒加载;懒汉式延迟至首次调用才初始化,需同步机制保障线程安全,现代推荐使用lazy实现。

饿汉式单例:类加载时就初始化,线程安全但不懒加载
直接在静态字段声明时创建实例,CLR 保证类首次被引用前完成初始化,天然线程安全,无需额外同步逻辑。
-
private static readonly EagerSingleton instance = new EagerSingleton();这行代码在类型加载阶段执行,不是第一次访问Instance属性时 - 构造函数必须是
private,否则外部仍可通过new绕过单例控制 - 类建议加
sealed,防止继承后子类绕过私有构造逻辑(虽非强制,但 JIT 优化和语义更清晰) - 如果构造函数里做了耗时操作(如读配置、连数据库),会拖慢应用启动,哪怕这个单例后续根本没被用到
Lazy 实现懒汉式:推荐的现代写法,线程安全且真正延迟初始化
.NET 4.0+ 的 Lazy<t></t> 内部已做双重检查 + 锁优化,比手写双检锁更可靠,也比全程加锁更轻量。
- 写法简洁:
private static readonly Lazy<lazysingleton> instance = new Lazy<lazysingleton>(() => new LazySingleton());</lazysingleton></lazysingleton> - 首次调用
instance.Value才触发构造,且保证只执行一次;后续调用直接返回缓存值 - 不用手动处理
volatile或lock,避免手写双检锁时漏掉第二层 null 判断(常见错误) - 注意:如果构造函数抛异常,
Lazy<t></t>会缓存该异常,后续再取Value仍会重抛——这和手写双检锁行为一致,但容易被忽略
双检锁(Double-Check Locking):老派但可控,容易写错
手动实现线程安全懒加载的经典方式,性能介于饿汉式和 Lazy<t></t> 之间,但极易出错。
- 必须用
volatile修饰实例字段(如private static volatile Singleton instance;),否则可能因 CPU 指令重排导致其他线程看到未完全构造的对象 - 外层
if (instance == null)是性能优化,避免每次访问都进锁;内层if (instance == null)才是真正防重复创建的关键 - 锁对象必须是
static readonly object,不能是this或实例字段,否则多线程下锁不住 - 如果忘了
volatile或漏掉内层判断,就会在高并发下偶发创建多个实例——这种 bug 很难复现,上线后才暴露
序列化场景下如何保持单例语义
默认序列化/反序列化会绕过构造函数,直接生成新对象,破坏单例。必须显式干预。
- 加
[Serializable]后,需同时实现ISerializable接口,并在反序列化构造函数中返回现有实例(如return Instance;) - 更简单的方式是加
[Serializable]+[OnDeserializing]方法,在反序列化前把字段设回Instance - 如果使用
Lazy<t></t>,记得序列化的是Lazy<t></t>对象本身,不是内部值;反序列化后需手动调用Value,否则仍为空 - 大多数现代项目已转向 DI 容器管理生命周期,序列化单例的需求本身就在变少——但若你真要序列化,这点绝不能跳过










