从最常见的非线程安全的开始介绍,到完全延迟加载、线程安全、简单且高性能的版本。
在 C# 中实现单例模式有多种方法,每种方法在线程安全性、性能特点和实现复杂度上各有不同。本文将详细介绍从最简单的非线程安全版本到完全延迟加载、线程安全且高性能的实现方案。
单例模式的核心特征
所有单例实现都应具备以下四个共同特征:
- 私有无参构造函数:防止外部类实例化,确保单例的唯一性。同时也能防止子类化带来的潜在问题
- 密封类(推荐):虽然不是绝对必要,但有助于 JIT 编译器进行优化
- 静态实例引用:保存单例的唯一实例
- 公共静态访问器:通过静态方法或属性提供全局访问点
注意:使用公共静态属性
Instance与使用静态方法在功能上没有区别,且不影响线程安全性或性能。
实现方案详解
版本一:非线程安全(不推荐使用)
public sealed class Singleton
{
private static Singleton instance = null;
private Singleton() { }
public static Singleton Instance
{
get
{
if (instance == null)
{
instance = new Singleton();
}
return instance;
}
}
}
问题:在多线程环境下,如果两个线程同时检查 instance == null 且都得到 true,则会创建多个实例,违反单例原则。
版本二:简单线程安全
public sealed class Singleton
{
private static Singleton instance = null;
private static readonly object padlock = new object();
private Singleton() { }
public static Singleton Instance
{
get
{
lock (padlock)
{
if (instance == null)
{
instance = new Singleton();
}
return instance;
}
}
}
}
优点:通过锁机制确保线程安全 缺点:每次访问实例都需要获取锁,性能开销较大
注意:这里使用私有静态对象作为锁对象,避免了使用
typeof(Singleton)可能导致的死锁问题。
版本三:双重检查锁定
public sealed class Singleton
{
private static Singleton instance = null;
private static readonly object padlock = new object();
private Singleton() { }
public static Singleton Instance
{
get
{
if (instance == null)
{
lock (padlock)
{
if (instance == null)
{
instance = new Singleton();
}
}
}
return instance;
}
}
}
优点:减少了锁的使用次数,提升性能 缺点:
- 在 Java 中不可靠(内存模型差异)
- 依赖于特定的内存模型语义
- 实现容易出错
- 性能仍不如后续版本
版本四:静态初始化(非完全延迟加载)
public sealed class Singleton
{
private static readonly Singleton instance = new Singleton();
// 显式静态构造函数,防止 beforefieldinit 标记
static Singleton() { }
private Singleton() { }
public static Singleton Instance => instance;
}
优点:
- 实现简单
- 线程安全(由 CLR 保证)
- 性能优秀
缺点:不是完全延迟加载,只要访问类的任何静态成员都会触发实例化
版本五:完全延迟加载
public sealed class Singleton
{
private Singleton() { }
public static Singleton Instance => Nested.instance;
private class Nested
{
// 显式静态构造函数
static Nested() { }
internal static readonly Singleton instance = new Singleton();
}
}
优点:
- 完全延迟加载
- 线程安全
- 高性能
缺点:实现相对复杂
版本六:使用 Lazy(.NET Framework 4+)
public sealed class Singleton
{
private static readonly Lazy<Singleton> lazy =
new Lazy<Singleton>(() => new Singleton());
public static Singleton Instance => lazy.Value;
private Singleton() { }
}
优点:
- 实现简洁
- 线程安全(默认使用
LazyThreadSafetyMode.ExecutionAndPublication) - 完全延迟加载
- 可通过
IsValueCreated属性检查是否已实例化
性能与延迟加载的考量
在大多数情况下,完全延迟加载并非必需。如果初始化过程不涉及特别耗时的操作,使用静态初始化(版本四)可能更合适,因为它允许 JIT 编译器进行更好的优化。
异常处理考虑
如果单例构造函数可能抛出异常且需要恢复,建议使用版本二的简单线程安全实现。类型初始化器在遇到异常后的行为在不同运行时中可能不一致,而使用锁和显式检查的方式提供了更可靠的异常处理机制。
总结
| 版本 | 线程安全 | 延迟加载 | 性能 | 推荐度 |
|---|---|---|---|---|
| 版本一 | ❌ | ✅ | 高 | ❌ 不推荐 |
| 版本二 | ✅ | ✅ | 低 | ⭐️ 基础可用 |
| 版本三 | ✅ | ✅ | 中 | ⭐️ 可用但有隐患 |
| 版本四 | ✅ | ❌ | 高 | ⭐️⭐️ 推荐(非延迟场景) |
| 版本五 | ✅ | ✅ | 高 | ⭐️⭐️⭐️ 推荐 |
| 版本六 | ✅ | ✅ | 高 | ⭐️⭐️⭐️⭐️ 强烈推荐(.NET 4+) |
对于新项目,如果目标平台支持 .NET Framework 4 或更高版本,推荐使用 Lazy<T> 实现,它在简洁性、安全性和性能之间取得了最佳平衡。