单例模式(Singleton Pattern)

本文最后更新于 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
{
// 使用 volatile 关键字确保赋值操作完成前,不会被指令重排序。
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> 已经为你完美地处理了这一切。

单例模式(Singleton Pattern)
http://example.com/2025/08/29/设计模式/创建型/单例模式/
作者
Charles
发布于
2025年8月29日
许可协议