网站首页 文章专栏 C# 线程安全的单例模式
从最常见的非线程安全的开始介绍,到完全延迟加载、线程安全、简单且高性能的版本。
在 C# 中实现单例模式有多种方法,每种方法在线程安全性、性能特点和实现复杂度上各有不同。本文将详细介绍从最简单的非线程安全版本到完全延迟加载、线程安全且高性能的实现方案。
所有单例实现都应具备以下四个共同特征:
注意:使用公共静态属性
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;
}
}
}
优点:减少了锁的使用次数,提升性能 缺点:
public sealed class Singleton
{
private static readonly Singleton instance = new Singleton();
// 显式静态构造函数,防止 beforefieldinit 标记
static Singleton() { }
private Singleton() { }
public static Singleton Instance => instance;
}
优点:
缺点:不是完全延迟加载,只要访问类的任何静态成员都会触发实例化
public sealed class Singleton
{
private Singleton() { }
public static Singleton Instance => Nested.instance;
private class Nested
{
// 显式静态构造函数
static Nested() { }
internal static readonly Singleton instance = new Singleton();
}
}
优点:
缺点:实现相对复杂
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> 实现,它在简洁性、安全性和性能之间取得了最佳平衡。
