C# 设计模式——单例模式
在C#中,单例设计模式(Singleton Pattern) 是一种创建型设计模式,核心目标是确保一个类在整个应用程序生命周期中只存在一个实例,并提供一个全局访问点供外部使用。这种模式适合管理共享资源(如配置文件、日志记录器、数据库连接池等),避免频繁创建实例导致的资源浪费或状态不一致。
单例模式的核心要素
要实现单例模式,必须满足以下3个条件:
- 私有构造函数:阻止外部通过
new
关键字创建实例。 - 静态私有实例:在类内部维护唯一的实例对象。
- 静态公共访问点:提供一个全局方法/属性,返回该唯一实例。
一、常见实现方式(从简单到线程安全)
单例模式的实现难点在于多线程环境下的线程安全(避免并发创建多个实例)和延迟初始化(按需创建实例,节省资源)。以下是几种典型实现:
1. 饿汉式(Eager Initialization)
特点:类加载时立即初始化实例(非延迟加载),天然线程安全(C#中静态构造函数由CLR保证线程安全)。
public sealed class SingletonEager
{// 1. 静态私有实例:类加载时直接初始化private static readonly SingletonEager _instance = new SingletonEager();// 2. 私有构造函数:阻止外部实例化private SingletonEager() { }// 3. 静态公共访问点:返回唯一实例public static SingletonEager Instance => _instance;
}
优缺点:
- 优点:实现简单,线程安全(无需额外处理)。
- 缺点:无论是否使用,实例都会在类加载时创建(若实例初始化耗时,会影响程序启动速度)。
2. 懒汉式(Lazy Initialization,非线程安全)
特点:延迟初始化(第一次使用时才创建实例),但多线程环境下可能创建多个实例(非线程安全)。
public sealed class SingletonLazyUnsafe
{// 1. 静态私有实例:初始为null(延迟初始化)private static SingletonLazyUnsafe _instance;// 2. 私有构造函数private SingletonLazyUnsafe() { }// 3. 静态访问点:第一次调用时创建实例public static SingletonLazyUnsafe Instance{get{if (_instance == null){_instance = new SingletonLazyUnsafe(); // 多线程并发时可能多次执行}return _instance;}}
}
问题:多线程同时调用Instance
时,若_instance
为null
,多个线程会同时进入if
语句,创建多个实例,违反单例原则。
适用场景:仅适用于单线程环境(如简单工具类)。
3. 线程安全的懒汉式(双重检查锁定,Double-Check Locking)
特点:结合延迟初始化和线程安全,通过lock
加锁和双重判断避免并发问题,是实际开发中最常用的实现之一。
public sealed class SingletonDoubleCheck
{// 1. 静态私有实例:用volatile修饰,防止指令重排序(关键!)private static volatile SingletonDoubleCheck _instance;// 2. 锁对象:用于线程同步private static readonly object _lock = new object();// 3. 私有构造函数private SingletonDoubleCheck() { }// 4. 静态访问点:双重检查+锁定public static SingletonDoubleCheck Instance{get{// 第一次检查:若实例已存在,直接返回(避免每次加锁,提高性能)if (_instance == null){// 加锁:确保同一时间只有一个线程进入初始化逻辑lock (_lock){// 第二次检查:防止多个线程等待锁时,已有线程创建了实例if (_instance == null){_instance = new SingletonDoubleCheck();}}}return _instance;}}
}
关键点解析:
volatile
关键字:防止编译器对_instance = new SingletonDoubleCheck()
进行指令重排序(该操作可分解为“分配内存→初始化对象→赋值给变量”,若重排序可能导致其他线程获取到未初始化的实例)。- 双重检查:第一次检查避免不必要的加锁(提高性能),第二次检查防止多线程等待锁时重复创建实例。
优缺点:
- 优点:延迟初始化、线程安全、性能优异(仅首次创建时加锁)。
- 缺点:实现稍复杂,需注意
volatile
和双重检查的细节。
4. 静态内部类(Lazy Initialization with Static Nested Class)
特点:利用C#静态内部类的特性实现延迟初始化和线程安全(推荐使用,简洁且无锁)。
public sealed class SingletonNested
{// 1. 私有构造函数private SingletonNested() { }// 2. 静态内部类:仅在被调用时加载private static class Nested{// 静态内部类的静态字段:由CLR保证线程安全,且仅在第一次访问时初始化internal static readonly SingletonNested Instance = new SingletonNested();}// 3. 公共访问点:调用内部类的实例public static SingletonNested Instance => Nested.Instance;
}
原理:
- 外部类
SingletonNested
加载时,内部类Nested
不会被加载。 - 第一次调用
Instance
时,内部类Nested
被加载,其静态字段Instance
被初始化(CLR保证静态字段初始化是线程安全的)。
优缺点:
- 优点:延迟初始化、线程安全、实现简洁(无需手动加锁)、性能好。
- 缺点:无法传递参数给单例的构造函数(若单例需要初始化参数,此方式不适用)。
5. 使用Lazy<T>
(.NET 4.0+ 推荐方式)
特点:利用.NET内置的Lazy<T>
类实现延迟初始化,自带线程安全机制,无需手动处理锁逻辑。
public sealed class SingletonLazy
{// 1. 用Lazy<T>包装实例:指定初始化方法,默认线程安全private static readonly Lazy<SingletonLazy> _lazyInstance = new Lazy<SingletonLazy>(() => new SingletonLazy());// 2. 私有构造函数private SingletonLazy() { }// 3. 公共访问点:通过Lazy<T>.Value获取实例public static SingletonLazy Instance => _lazyInstance.Value;
}
Lazy<T>
的线程安全:
- 默认情况下,
Lazy<T>
使用LazyThreadSafetyMode.ExecutionAndPublication
模式,确保多线程环境下只初始化一次。 - 若需自定义线程安全策略,可通过
Lazy<T>
的构造函数参数指定(如LazyThreadSafetyMode.None
关闭线程安全,适合单线程)。
优缺点:
- 优点:延迟初始化、线程安全(内置处理)、代码简洁、支持传递参数(通过
Lazy<T>
的初始化委托)。 - 缺点:依赖.NET 4.0及以上框架(现代项目基本都满足)。
二、单例模式的注意事项
-
防止反射攻击:
私有构造函数可能被反射强行调用(如typeof(Singleton).GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance).Invoke(null)
),导致创建多个实例。
防护方式:在构造函数中检查实例是否已存在,若存在则抛异常:private SingletonLazy() {if (_lazyInstance.IsValueCreated){throw new InvalidOperationException("单例实例已存在,禁止重复创建");} }
-
序列化问题:
若单例类需要序列化(实现ISerializable
),反序列化时可能创建新实例。需重写GetObjectData
和反序列化构造函数,确保返回原实例:public void GetObjectData(SerializationInfo info, StreamingContext context) {// 序列化时不存储数据,反序列化时返回单例 }private SingletonLazy(SerializationInfo info, StreamingContext context) {// 反序列化时强制返回现有实例if (_lazyInstance.IsValueCreated){throw new InvalidOperationException("禁止反序列化创建单例");} }
-
单例的生命周期:
单例实例通常随应用程序生命周期存在(除非手动释放),适合管理全局资源。但过度使用会导致代码耦合度升高(依赖全局状态),不利于单元测试。
三、适用场景
- 全局配置管理(如
AppConfig
):确保配置只加载一次,全局共享。 - 日志记录器(如
Logger
):避免多个日志实例导致的文件锁冲突。 - 数据库连接池:统一管理连接,防止连接数爆炸。
- 缓存管理器:全局缓存实例,保证缓存数据一致性。
总结
单例模式的核心是“唯一实例 + 全局访问”,在C#中推荐使用以下两种实现:
- 简单场景(无参数、需简洁):静态内部类或**
Lazy<T>
**。 - 复杂场景(需线程安全、可能传递参数):双重检查锁定或**
Lazy<T>
**(Lazy<T>
更推荐,内置线程安全)。
使用时需注意线程安全、反射攻击和序列化问题,避免滥用单例导致代码灵活性下降。