21. 单例模式
约 1894 字大约 6 分钟
2026-03-23
定义
单例模式(Singleton),保证一个类仅有一个实例,并提供一个访问它的全局访问点。
人话:迭代器模式就像公交车上的售票员查票的过程。公交车上坐着很多乘客,比如“大鸟”“小菜”“老外”等,这些乘客都被放在公交车这个“集合”里。
如果不用迭代器模式来设计,程序里可能就要直接操作这个集合,比如用下标一个个访问乘客,还要自己去控制从第几个开始、什么时候结束、有没有越界等等。这样不仅麻烦,而且一旦集合结构发生变化(比如从数组换成链表),遍历代码也要跟着改,这在软件中就属于遍历逻辑与集合结构耦合过高。
而使用迭代器模式之后,就相当于安排了一位“售票员”。公交车本身只负责存放乘客,而“售票员”负责按顺序一个一个去询问乘客有没有买票。售票员知道如何从第一个乘客开始、如何走到下一个、以及什么时候走到最后。
这样一来,客户端代码就不需要关心公交车内部是怎么存储乘客的,也不用自己写循环控制逻辑,只需要让“售票员”去遍历即可,比如不断调用“下一个”,直到结束。
在软件中也是一样:迭代器模式把“如何遍历集合”这件事单独封装起来,让我们可以用统一的方式访问集合中的元素,而不需要暴露集合的内部结构,从而让代码更加清晰、解耦、易于扩展。
单例模式(Singleton)结构图
Singleton类——单例类:
// Singleton(单例类)
class Singleton
{
// 静态实例
private static Singleton instance;
// 构造方法私有化,禁止外部 new
private Singleton()
{
}
// 获取实例的唯一入口(全局访问点)
public static Singleton GetInstance()
{
// 如果实例不存在,则创建
if (instance == null)
{
instance = new Singleton();
}
// 返回唯一实例
return instance;
}
}客户端代码:
static void Main(string[] args)
{
Singleton s1 = Singleton.GetInstance();
Singleton s2 = Singleton.GetInstance();
// 判断两个实例是否相同
if (s1 == s2)
{
Console.WriteLine("两个对象是相同的实例。");
}
Console.Read();
}通常我们可以让一个全局变量使得一个对象被访问,但它不能防止你实例化多个对象。一个最好的办法就是,让类自身负责保存它的唯一实例。这个类可以保证没有其他实例可以被创建,并且它可以 提供一个访问该实例的方法。
单例模式因为Singleton类封装它的唯一实例,这样它可以严格地控制客户怎样访问它以及何时访问它。简单地说就是对唯一实例的受控访问。
实用类通常也会采用私有化的构造方法来避免其有实例。但它们还是有很多不同的,比如实用类不保存状态,仅提供一些静态方法或静态属性让你使用,而单例类是有状态的。实用类不能用于继承多态,而单例虽然实例唯一,却是可以有子类来继承。实用类只不过是一些方法属性的集合,而单例却是有着唯一的对象实例。在运用中还得仔细分析再作决定用哪一种方式。
多线程(懒汉)
另外,多线程的程序中,多个线程同时,注意是同时访问Singleton 类,调用GetInstance0方法,会有可能造成创建多个实例的。可以给进程一把锁来处理。
lock语句的涵义:lock是确保当一个线程位于代码的临界区时,另一个线程不进入临界区。如果其他线程试图进入锁定的代码,则它将一直等待(即被阻止),直到该对象被释放。
加锁
Singleton类——线程安全单例(加锁版):
// Singleton(线程安全单例 - 加锁版)
class Singleton
{
// 唯一实例
private static Singleton instance;
// 线程同步锁对象(静态只读)
private static readonly object syncRoot = new object();
// 构造方法私有化,禁止外部创建
private Singleton()
{
}
// 获取实例(线程安全)
public static Singleton GetInstance()
{
lock (syncRoot) // 同一时刻只允许一个线程进入
{
if (instance == null)
{
instance = new Singleton();
}
}
return instance;
}
}双重锁定
Singleton类——线程安全单例(双重锁定版):
// Singleton(线程安全单例 - 双重锁定版)
class Singleton
{
// 唯一实例
private static Singleton instance;
// 线程同步锁对象(静态只读)
private static readonly object syncRoot = new object();
// 构造方法私有化,禁止外部创建
private Singleton()
{
}
// 获取实例(线程安全)
public static Singleton GetInstance()
{
if (instance == null) {
lock (syncRoot) // 同一时刻只允许一个线程进入
{
if (instance == null)
{
instance = new Singleton();
}
}
}
return instance;
}
}现在这样,我们不用让线程每次都加锁,而只是在实例未被创建的时候再加锁处理。同时也能保证多线程的安全。这种做法被称为 Double-CheckLocking (双重锁定)。
在外面已经判断了 instance 实例是否存在,为什么在lock里面还需要做一次 instance 实例是否存在的判断呢?对于 instance 存在的情况,就直接返回,这没有问题。当 instance 为 null 并且同时有两个线程调用
GetInstance()方法时,它们将都可以通过第一重instance == null的判断。然后由于 lock 机制,这两个线程则只有一个进入,另一个在外排队等候,必须要其中的一个进入并出来后,另一个才能进入。而此时如果没有了第二重的 instance 是否为 null 的判断,则第一个线程创建了实例,而第二个线程还是可以继续再创建新的实例,这就没有达到单例的目的。
静态初始化(饿汉)
Singleton类——静态初始化单例:
// Singleton(静态初始化版)
sealed class Singleton // sealed 阻止发生派生
{
// 静态只读实例,在类加载时创建(线程安全)
private static readonly Singleton instance = new Singleton();
// 构造方法私有化,防止外部实例化
private Singleton()
{
}
// 全局访问点
public static Singleton GetInstance()
{
return instance;
}
}instance 变量标记为 readonly ,这意味着只能在静态初始化期间或在类构造函数中分配变量。由于这种静态初始化的方式是在自己被加载时就将自己实例化,所以被形象地称之为饿汉式单例类,原先的单例模式处理方式是要在第一次被引用时,才会将自己实例化,所以就被称为懒汉式单例类。
由于饿汉式,即静态初始化的方式,它是类一加载就实例化的对象,所以要提前占用系统资源。然而懒汉式,又会面临着多线程访问的安全性问题,需要做双重锁定这样的处理才可以保证安全。所以到底使用哪一种方式,取决于实际的需求。从C#语言角度来讲,饿汉式的单例类已经足够满足我们的需求了。
