请阐述在 C# 中实现线程安全单例模式的常用方法,并对比其优缺点及适用场景。
考察说明
考查对单例模式线程安全实现机制及性能取舍的理解。
回答思路
- 【回答框架 1】单例模式保证一个类仅有一个实例,并提供全局访问点。线程安全实现需处理多线程同时首次访问时的实例创建竞争。常用方法包括:锁(lock)、静态初始化(beforefieldinit)、双重检查锁定(Double-Check Locking)及 Lazy<T>。
- 【回答框架 2】锁实现:在公有静态属性中,使用 lock 语句包裹实例创建逻辑。此方法简单可靠,但每次访问都会加锁,性能开销较大。静态初始化:依赖 .NET 类型初始化器的线程安全保证,CLR 确保类型初始化只执行一次,代码简洁,但实例创建时机不受控,可能在首次访问类型时创建。
- 【回答框架 3】双重检查锁定:先无锁检查实例是否为空,为空才加锁,锁内再次检查,减少锁竞争。但需注意内存模型,C# 中 volatile 关键字可防止指令重排,确保实例字段的可见性。Lazy<T>:使用 Lazy<T> 的默认模式(ExecutionAndPublication)提供线程安全的延迟初始化,内部处理锁和双重检查,代码简洁且性能好。
- 【回答框架 4】推荐优先使用静态初始化或 Lazy<T>,它们既保证线程安全又简洁。锁实现适用于需要完全控制创建时机和资源释放的场景。双重检查锁定在早期 .NET 版本需谨慎,但在现代 .NET 中结合 volatile 已安全。
- 【关键点 1】静态初始化依赖 CLR 的类型初始化锁,保证线程安全且性能好。
- 【关键点 2】Lazy<T> 默认线程安全模式,适合延迟加载和简洁实现。
- 【关键点 3】双重检查锁定需结合 volatile 字段防止指令重排,避免错误的对象发布。
- 【关键点 4】锁实现简单但每次访问有同步开销,性能最低。
- 【易错点 1】忽略 volatile 可能导致双重检查锁定的实例字段发布不安全,产生部分构造对象。
- 【易错点 2】静态初始化可能提前创建实例,如果实例构造有副作用或依赖顺序,需考虑创建时机。
- 【易错点 3】过度设计:若无多线程访问需求,不应牺牲性能使用锁或复杂机制。