本文最后更新于 2025年8月29日 上午
定义
保证一个类仅有一个实例,并提供一个访问它的全局访问点。 就是整个进程有且仅有一个实例。 该类负责创建自己的对象,同时确保只有一个对象被创建。
主要目的
控制实例数量:防止创建多个对象,节省资源。
全局访问:提供一种方式让其他代码可以轻松访问这个唯一实例。
实现单例三步骤
- 私有化构造函数,防止被实例化。
- 对外提供获取实例的方法或属性。
- 提供静态变量,实例重用。
实现方式
1、简单实现 (❌非线程安全)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| public class Singleton { private static Singleton? _instance = null;
private Singleton() { Console.WriteLine($"对象初始化,线程:{Environment.CurrentManagedThreadId}"); } public static Singleton Instance { get { _instance ??= new Singleton(); return _instance; } } public void DoSomething() { Console.WriteLine($"DoSomething,线程:{Environment.CurrentManagedThreadId}"); } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| for (int i = 0; i < 10; i++) { Singleton singleton = Singleton.Instance; singleton.DoSomething(); }
for (int i = 0; i < 10; i++) { Task.Run(() => { Singleton singleton = Singleton.Instance; singleton.DoSomething(); }); }
|
![[单例模式(Singleton Pattern)-1756374107258.png]]
![[单例模式(Singleton Pattern)-1756373414528.png]]
问题:如果两个线程同时执行到 if (_instance == null) 并且都发现为 null,那么它们都会创建新的实例,从而违反了单例原则。
2、使用锁解决线程安全问题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| public class Singleton { private static Singleton? _instance = null;
private static readonly object _lock = new();
private Singleton() { Console.WriteLine($"对象初始化,线程:{Environment.CurrentManagedThreadId}"); }
public static Singleton Instance { get { lock (_lock) { _instance ??= new Singleton(); return _instance; } } }
public void DoSomething() { Console.WriteLine($"DoSomething,线程:{Environment.CurrentManagedThreadId}"); } }
|
1 2 3 4 5 6 7 8
| for (int i = 0; i < 10; i++) { Task.Run(() => { Singleton singleton = Singleton.Instance; singleton.DoSomething(); }); }
|
![[单例模式(Singleton Pattern)-1756374449615.png]]
问题:性能开销大。每次获取实例时都需要加锁,即使实例早已创建。而锁操作在高并发场景下成本很高。
3、双重检查锁定
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| public class Singleton { private static volatile Singleton? _instance = null; private static readonly object _lock = new();
private Singleton() { Console.WriteLine($"对象初始化,线程:{Environment.CurrentManagedThreadId}"); }
public static Singleton Instance { get { if (_instance == null) { lock (_lock) { _instance ??= new Singleton(); } } return _instance; } }
public void DoSomething() { Console.WriteLine($"DoSomething,线程:{Environment.CurrentManagedThreadId}"); } }
|
优点:
- 线程安全。
- 只有在实例未创建时才会进入锁,性能远优于简单加锁版本。
注意:volatile 关键字在 .NET 中至关重要。它防止了 CPU 的指令重排序,确保 _instance 的赋值在构造函数完全执行完成后才发生,避免了其他线程获取到一个未初始化完成的对象。(在 .NET 4.0 及更高版本中,此模式已得到很好的定义)。
4、静态初始化
.NET CLR (公共语言运行时) 保证静态字段的初始化是线程安全的
4.1、静态构造函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| public class Singleton { private static Singleton _instance = null;
private Singleton() { Console.WriteLine($"对象初始化,线程:{Environment.CurrentManagedThreadId}"); } static Singleton() { _instance = new Singleton(); }
public static Singleton Instance => _instance;
public void DoSomething() { Console.WriteLine($"DoSomething,线程:{Environment.CurrentManagedThreadId}"); } }
|
4.2、静态字段
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| public class Singleton { private static Singleton _instance = new();
private Singleton() { Console.WriteLine($"对象初始化,线程:{Environment.CurrentManagedThreadId}"); }
public static Singleton Instance => _instance;
public void DoSomething() { ## Console.WriteLine($"DoSomething,线程:{Environment.CurrentManagedThreadId}"); } }
|
优点:
- 线程安全:由 CLR 保证。
- 简洁高效:没有显式的锁,代码非常干净。
- 惰性初始化 (Lazy Initialization):虽然看起来是饿汉式,但在 C# 中,静态字段是在第一次访问该类任何成员之前初始化的,所以它实际上是惰性的。
static Singleton() 构造函数的存在确保了 _instance 只有在第一次引用 Singleton 的某个静态成员(如 Instance 属性)时才会被创建。
5、使用 .NET 4.0+ 的 Lazy<T> 类型 (最佳实践)
Lazy<T> 类是专门为惰性初始化而设计的,它默认就是线程安全的,并且提供了统一的惰性初始化模式。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| public class Singleton { private static readonly Lazy<Singleton> _lazy = new(() => new Singleton());
public static Singleton Instance { get { return _lazy.Value; } }
private Singleton() { Console.WriteLine($"对象初始化,线程:{Environment.CurrentManagedThreadId}"); }
public void DoSomething() { Console.WriteLine($"DoSomething,线程:{Environment.CurrentManagedThreadId}"); } }
|
优点:
- 线程安全:
Lazy<T> 内部已经处理好了所有线程同步问题。
- 真正的惰性初始化:只有在第一次访问
_lazy.Value 时才会创建实例。
- 性能优异:其内部实现使用了双重检查锁定等优化策略。
- 代码极其简洁和表达性强:一眼就能看出这是惰性加载的单例。
- 支持异常处理:如果构造函数抛出异常,
Lazy<T> 会缓存这个异常,后续每次访问 Value 都会再次抛出,而不是尝试重新创建。
总结与选择
| 方法 |
线程安全 |
惰性初始化 |
性能 |
推荐度 |
| 1. 简单实现 |
❌ 否 |
✅ 是 |
高 (但不安全) |
❌ 绝不使用 |
| 2. 简单加锁 |
✅ 是 |
✅ 是 |
差 (每次获取都加锁) |
❌ 不推荐 |
| 3. 双重检查锁 |
✅ 是 |
✅ 是 |
良好 |
⭕ 可用,但稍复杂 |
| 4. 静态初始化 |
✅ 是 |
⚠️ (由 CLR 控制) |
优秀 |
✅ 推荐 (旧版 .NET) |
5. Lazy<T> |
✅ 是 |
✅ 是 |
优秀 |
🏆 强烈推荐 (.NET 4.0+) |
最终建议:
- 如果你的项目运行在 .NET Framework 4.0 或更高版本(包括 .NET Core, .NET 5/6/7+),请毫不犹豫地使用
Lazy<T>。它是最现代、最安全、最简洁且性能优异的选择。
- 如果出于某些原因你必须在旧版本的 .NET (如 2.0, 3.5) 上工作,那么静态初始化(方式4) 是最佳选择。
- 尽量避免自己手动实现复杂的双重检查锁定,除非你有非常特殊的性能调优需求,并且完全理解其中的内存屏障和指令重排序问题。
Lazy<T> 已经为你完美地处理了这一切。