21. 单例模式
约 1933 字大约 6 分钟
2026-03-23
定义
单例模式(Singleton),保证一个类仅有一个实例,并提供一个访问它的全局访问点。
人话:单例模式就像以前的“计划生育”场景,一个家庭到底要不要再生孩子、能不能生,其实是由家庭自己控制的,而不是外界随便就能决定。也就是说,“是否创建新的实例”这件事,是对象自己负责的。
如果不用单例模式来设计,程序里任何地方都可以随意 new 一个对象,比如想用就 new 一个,这样就可能导致系统中出现很多个实例。对于某些对象来说(比如配置管理、日志系统、线程池等),出现多个实例不仅浪费资源,还可能导致数据不一致,这在软件中就属于实例失控、资源浪费、状态不一致。
而使用单例模式之后,就相当于把“能不能创建实例”的权力收归到类自己手里。类会把构造方法设为私有,外部无法直接 new,只能通过一个统一的入口(比如 GetInstance())来获取实例。类内部会判断:如果还没有实例,就创建一个;如果已经有了,就直接返回已有的那个。
这样一来,不管程序中有多少地方在使用这个类,最终拿到的都是同一个对象。客户端不需要关心对象是怎么创建的,也不需要自己控制实例数量,只需要通过统一的入口获取即可。
在软件中也是一样:单例模式通过控制实例化过程,让系统中某个类始终只有一个对象存在,同时提供一个全局访问点,从而避免重复创建、保证数据一致性,并让资源使用更加可控。
单例模式(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#语言角度来讲,饿汉式的单例类已经足够满足我们的需求了。
