C# 设计模式|单例模式全攻略:从基础到高级实现与防御
标签::Lazy, volatile, Interlocked, 依赖注入, 反射, 序列化,延迟初始化, 线程安全, 类加载机制
目录
- 1 核心思想
- 1.1 概述
- 1.2 三要素
- 1.3 优点
- 1.4 缺点
- 1.5 典型场景
- 2. 演进路线与实现
- 2.1 基础分型
- 2.2 技术演进顺序分析 7种典型实现
- 3 扩展
- 3.1 整合“继承、委托、泛型与配置系统”
- 3.1.1 参数化单例
- 3.1.2 支持子类化扩展
- 3.1.3 多例模式
- 3.1.4 泛型单例模板
- 3.1.5 与静态类对比
- 3.2 其他
- 3.2.1 异步单例
- 3.2.2 依赖注入友好的单例
- 3.2.3 示例
- 3.2.4 最佳实践建议
- 3.2.5 线程级单例(ThreadLocal)
- 3.2.6 集群/分布式单例
- 4 破坏单例的场景及防御
- 4.1 破坏单例的场景及防御措施
- 4.1.2 反射破坏
- 1 反射攻击原理与示例
- 2 防御方案
- 3 基础防御方案:静态标志位检查
- 4 线程安全加固方案
- 5 局限性
- 4.1.2 序列化/反序列化破坏
- 1 破坏原理
- 2 防御方案
- 4.1.3 克隆破坏
- 4.1.4 最佳实践:依赖注入容器
- 4.1.3 避免滥用单例(全局变量陷阱)
- 4.2 总结
- 5 相关模式
- 6 总结
1 核心思想
1.1 概述
-
目的
保证一个类在进程生命周期内仅产生一个实例,并为全系统提供统一、可控的访问入口。
-
两个刚性约束
① 唯一性:任何时刻最多只有一个对象。
② 全局可达:通过类级接口(通常是静态方法)即可拿到该对象,无需自行 new。
-
为什么需要它(直接价值)
-
资源节约
避免重复创建重量级对象(线程池、连接池、配置中心、缓存、日志管理器等)。
-
状态一致性
全系统共享同一份内部状态,杜绝“多实例数据漂移”问题(例如两处显示不同 CPU 使用率)。
-
协调逻辑
可作为全局协调者(序列号生成器、打印机后台服务、操作系统窗口管理器等)。
-
1.2 三要素
本质: 将类的实例化控制权从外部(调用方)转移到类自身内部,由类自己决定何时创建以及如何保证唯一性。
-
私有化构造函数
private Singleton() { }
禁止外部通过
new
创建实例。 -
静态私有字段保存唯一实例: 类内部定义一个静态变量来保存它创建的唯一实例。
private static Singleton _instance;
保存唯一实例。
-
公共静态属性/方法提供访问点: 外部代码通过调用这个方法来获取单例实例。这个方法负责控制实例的创建(只在第一次调用时创建)和返回。
public static Singleton Instance{get{// 简单版本if (_instance == null){_instance = new Singleton();}return _instance;}}//可二选一public static Singleton GetInstance() {// 简单版本if (_instance == null){_instance = new Singleton();}return _instance;}
全局唯一访问入口。
1.3 优点
- 资源节约:减少频繁创建和销毁对象的开销
- 全局访问:提供统一的访问点,方便管理
- 状态一致:保证实例状态的全局一致性
1.4 缺点
- 职责过重:容易违背单一职责原则,成为"上帝对象"
- 测试困难:高耦合度导致单元测试复杂
- 隐藏依赖:全局状态使得代码依赖关系不明确
1.5 典型场景
- 单例模式通常用于管理需要全局协调访问的稀缺或共享资源。以下是一些典型应用场景:
- 🌐 配置中心
- 📝 日志组件
- 🗃️ 数据库连接池
- 🧵 线程池
- 🧊 缓存管理器
- 📅 任务调度器
- 🆔 唯一序列号生成器
- 🖨️ 硬件设备管理器
这些场景的共同特点是都涉及具有全局唯一性或需要严格控制访问的资源,必须确保整个应用中对这些资源的访问是协调、一致的。
2. 核心判断依据
考虑是否使用单例模式时,可基于以下两个关键问题进行判断:
- 该类的对象是否在系统中必须且只能存在一个实例?
- 该类是否负责管理需要全局共享和协调访问的状态或资源?
如果以上两个问题的答案均为“是”,则该类适合设计为单例。
2. 演进路线与实现
graph TD
A[单例模式] --> B[饿汉式]
A --> C[懒汉式]
B --> D[基础静态初始化]
C --> F[基础非线程安全版]
C --> G[锁机制实现]
G --> H[粗粒度锁]
G --> I[双重检查锁]
C --> J[CLR安全机制]
J --> K[静态内部类]
C --> L[现代语言特性]
C --> N[现代应用] --> O[DI容器]
L --> M[Lazy<T>]
2.1 基础分型
- 🍔 饿汉式,Eager Initialization:空间换时间(启动即初始化)
- 🛌🏻 懒汉式,Lazy Initialization:时间换空间(按需初始化)
- 饿汉式 vs 懒汉式
类型 饿汉式 懒汉式 实例化时机 类加载时立即创建 首次调用`getInstance()`时创建 线程安全 天然线程安全 需额外控制(如双重检查锁定) 资源占用 可能浪费资源(未用也加载) 按需加载,节约资源 性能 访问速度快 首次调用慢,线程安全影响性能
2.2 技术演进顺序分析 7种典型实现
-
1.基础|属性初始化|懒汉|非线程安全
public sealed class Singleton0 {private static Singleton0 _instance;private Singleton0() { }public static Singleton0 Instance => _instance ??= new Singleton0(); }
- 优点:代码简洁,延迟初始化
- 缺点:多线程环境下可能创建多个实例
- 适用场景:单线程环境或不需要严格单例的场景
-
2.基础|CLR安全机制|静态字段初始化|饿汉|线程安全
public sealed class Singleton3 {private static readonly Singleton3 _instance = new(); // 启动即初始化private Singleton3() { }public static Singleton3 Instance => _instance; }
- 优点:
- 线程绝对安全(CLR保障)
- 无锁访问性能最佳
- 缺点:
- 程序启动即初始化,可能浪费资源
- 初始化失败会导致类型不可用
- 适用场景:初始化耗时短且必然使用的实例
- 优点:
-
3.基础|CLR安全机制|静态内部类|懒汉 |线程安全
public sealed class Singleton {private Singleton() { }public static Singleton Instance => Nested._instance;private class Nested{internal static readonly Singleton _instance = new Singleton();} }
- 原理:静态字段在首次访问时初始化,由 CLR 保障线程安全。
- 优点:
- 延迟初始化(首次访问时创建)
- 无锁高性能
-
4.锁机制|粗粒度锁|属性初始化|懒汉|简单线程安全版
public sealed class Singleton1 {private static Singleton1 _instance;private static readonly object _lock = new();private Singleton1() { }public static Singleton1 Instance {get {lock (_lock) { // 全程加锁,性能差return _instance ??= new Singleton1();}}} }
- 解决线程安全问题
- 缺点:每次访问都加锁,99% 的读操作无需锁,优化方向:减少锁竞争
-
5.锁机制|双重检查锁(DCL,Double-Checked Locking)|属性初始化|懒汉|线程安全版
/// <summary> /// 双重检查锁(Double-Checked Locking)实现单例模式 /// </summary> public sealed class Singleton2 {// 1. volatile 关键字确保多线程环境下的可见性和有序性// - 防止JIT/CPU指令重排序(避免返回部分初始化的对象)// - 确保写入操作完成后立即刷新到主内存private static volatile Singleton2 _instance; // 2. 线程同步锁对象(readonly保证线程安全初始化)// - 使用专用锁对象而非类型本身,避免外部锁定导致的死锁风险private static readonly object _lock = new();// 3. 私有构造函数(阻止外部实例化)private Singleton2() { } /// <summary>/// 全局访问点/// </summary>public static Singleton2 Instance {get {// 第一重检查:无锁快速路径// - 避免已成单例时的锁竞争(99%的情况直接返回)if (_instance == null) { lock (_lock) // 进入同步区域{// 第二重检查:加锁后确认// - 防止多个线程同时通过第一重检查后重复创建if (_instance == null) { // 创建实例(volatile确保写操作不被重排序)_instance = new Singleton2();}}}return _instance;}} }
- 关键技术:
volatile
禁止指令重排序(避免返回未初始化对象)- 双重null检查减少锁竞争
- 优点:
- 延迟初始化
- 线程安全
- 高性能(仅首次初始化需要锁)
- 关键技术:
-
6. 现代语言特性| Lazy<T>|线程安全
详细内容可参考另一篇:.NET 中的延迟初始化:Lazy<T> 与LazyInitializer-CSDN博客
public sealed class Singleton4 {private static readonly Lazy<Singleton4> _lazy = new(() => new Singleton4(), LazyThreadSafetyMode.ExecutionAndPublication);private Singleton4() { }public static Singleton4 Instance => _lazy.Value; // 延迟+线程安全+高性能 }
-
优势:
- 原生线程安全,支持延迟初始化。
- 避免手动处理锁逻辑。
-
参数扩展:
LazyThreadSafetyMode.ExecutionAndPublication
提供默认安全模式。
-
-
7. 现代应用|依赖注入容器|线程安全|企业级标准
-
安装
dotnet add package Microsoft.Extensions.DependencyInjection dotnet add package Microsoft.Extensions.Logging.Console
-
代码
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging;#region// 设置依赖注入容器 var serviceCollection = new ServiceCollection();// 配置日志 serviceCollection.AddLogging(builder => {builder.AddConsole();builder.SetMinimumLevel(LogLevel.Information); });// 注册单例服务 serviceCollection.AddSingleton<ISingletonService, SingletonService>();// 注册其他服务(瞬态生命周期) serviceCollection.AddTransient<RequestProcessor>();// 构建服务提供程序 var serviceProvider = serviceCollection.BuildServiceProvider();// 获取日志器 var logger = serviceProvider.GetService<ILogger<Program>>(); logger.LogInformation("Application started");// 模拟多个线程使用单例服务 var tasks = new List<Task>();for (int i = 0; i < 5; i++) {int threadId = i;tasks.Add(Task.Run(() =>{// 每个线程获取自己的RequestProcessor实例var processor = serviceProvider.GetService<RequestProcessor>();processor.ProcessMultipleRequests(3);logger.LogInformation("Thread {ThreadId} completed", threadId);})); }// 等待所有任务完成 await Task.WhenAll(tasks);// 直接获取单例服务并显示统计信息 var singletonService = serviceProvider.GetService<ISingletonService>(); Console.WriteLine("\n=== Final Statistics ==="); Console.WriteLine(singletonService.GetStatistics());// 验证确实是同一个实例 var sameSingletonService = serviceProvider.GetService<ISingletonService>(); Console.WriteLine($"\nSame instance? {singletonService.Id == sameSingletonService.Id}");logger.LogInformation("Application completed"); #endregion# region ISingletonService public interface ISingletonService {Guid Id { get; }int RequestCount { get; }void ProcessRequest(string requestData);string GetStatistics(); } #endregion# region SingletonService public class SingletonService : ISingletonService {private readonly ILogger<SingletonService> _logger;private int _requestCount = 0;private readonly object _lock = new object();public Guid Id { get; } = Guid.NewGuid();public int RequestCount => _requestCount;public SingletonService(ILogger<SingletonService> logger){_logger = logger;_logger.LogInformation("SingletonService initialized with ID: {Id}", Id);}public void ProcessRequest(string requestData){lock (_lock){_requestCount++;_logger.LogInformation("Processing request #{RequestCount}: {RequestData}",_requestCount, requestData);// 模拟处理时间Thread.Sleep(100);}}public string GetStatistics(){return $"Service ID: {Id}, Total Requests: {_requestCount}";} } #endregion#region RequestProcessorpublic class RequestProcessor {private readonly ISingletonService _singletonService;private readonly ILogger<RequestProcessor> _logger;private readonly Random _random = new Random();public RequestProcessor(ISingletonService singletonService, ILogger<RequestProcessor> logger){_singletonService = singletonService;_logger = logger;}public void ProcessMultipleRequests(int count){_logger.LogInformation("Starting to process {Count} requests", count);for (int i = 0; i < count; i++){var requestData = $"Request-{i}-{_random.Next(1000)}";_singletonService.ProcessRequest(requestData);}_logger.LogInformation("Finished processing requests");} } #endregion
-
输出
info: Program[0]Application started info: SingletonService[0]SingletonService initialized with ID: 9d2a8d6f-1c48-4127-8b5a-b240a9bcd427 info: RequestProcessor[0]Starting to process 3 requests info: RequestProcessor[0]Starting to process 3 requests info: RequestProcessor[0]Starting to process 3 requests info: RequestProcessor[0]Starting to process 3 requests info: RequestProcessor[0]Starting to process 3 requests info: SingletonService[0]Processing request #1: Request-0-386 info: SingletonService[0]Processing request #2: Request-0-652 info: SingletonService[0]Processing request #3: Request-0-378 info: SingletonService[0]Processing request #4: Request-0-839 info: SingletonService[0]Processing request #5: Request-0-276 info: SingletonService[0]Processing request #6: Request-1-278 info: SingletonService[0]Processing request #7: Request-1-975 info: SingletonService[0]Processing request #8: Request-1-835 info: SingletonService[0]Processing request #9: Request-1-737 info: SingletonService[0]Processing request #10: Request-1-283 info: SingletonService[0]Processing request #11: Request-2-50 info: RequestProcessor[0]Finished processing requests info: Program[0]Thread 2 completed info: SingletonService[0]Processing request #12: Request-2-675 info: RequestProcessor[0]Finished processing requests info: SingletonService[0]Processing request #13: Request-2-121 info: Program[0]Thread 3 completed info: SingletonService[0]Processing request #14: Request-2-683 info: RequestProcessor[0]Finished processing requests info: Program[0]Thread 1 completed info: RequestProcessor[0]Finished processing requests info: SingletonService[0]Processing request #15: Request-2-969 info: Program[0]Thread 0 completed info: RequestProcessor[0]Finished processing requests info: Program[0]Thread 4 completed=== Final Statistics === Service ID: 9d2a8d6f-1c48-4127-8b5a-b240a9bcd427, Total Requests: 15Same instance? True info: Program[0]Application completed
- 单例服务只被初始化一次(只有一个ID)
- 多个线程共享同一个单例服务实例
- 请求计数是线程安全的,正确累加
-
-
线程安全 & 性能对比
实现方式 懒加载 线程安全 性能 首次调用耗时 适用场景 推荐级别 1 简单模式 ✅ 支持 ❌ 非安全 ⚡️ 极高(无锁) 低 最简单,但仅限单线程 ⭐ 2 简单加锁 ✅ 支持 ✅ 安全(锁) 🐢 差(每次访问加锁) 高(锁竞争严重) 低并发场景 ⭐⭐ 4 双重检查锁(DCL) ✅ 支持 ✅ 安全(锁) ⚡️ 高 (锁竞争少,首次访问后无锁) 中(仅首次同步) 高并发场景(需`volatile`保证),经典线程安全懒汉式,逻辑稍复杂 ⭐⭐⭐⭐ 3 静态内部类 ✅ 支持 ✅ 安全 (CLR) ⚡️ 极高(无锁) 首次加载类时略高 延迟加载与无锁性能的完美结合 ⭐⭐⭐⭐⭐ 5 **Lazy\<T>** (.NET) ✅ 支持 ✅ 安全(内置) ⚡️ 高(内部优化锁) 低至中 现代、简洁、安全,微软官方推荐 ⭐⭐⭐⭐⭐ 6 依赖注入容器 ✅ 支持 ✅ 安全 (容器) ⚡️ 高 (有轻微容器开销) 企业级标准做法,集成、解耦、可管理 ⭐⭐⭐⭐⭐ 7 静态字段初始化 ❌ 不支持 ✅ 安全 (CLR) ⚡️ 极高(无锁) 程序启动时高 启动即初始化,CLR保障安全,初始化简单、提前加载无影响的场景 ⭐⭐⭐
3 扩展
3.1 整合“继承、委托、泛型与配置系统”
单例模式(Singleton)并非仅仅旨在限制类的实例化次数,更在于为“受控对象生命周期”提供起点。
C# 通过整合“继承、委托、泛型与配置系统”等一系列特性,能够将单例模式扩展以下形态:
- 支持参数化扩展::根据不同的初始化参数创建不同的单例实例
- 支持子类化扩展:可在运行时动态决定具体类型;
- 支持数量扩展,演变为多例模式(Multiton);
- 支持泛型模板化:实现一次编码,多类型复用。
3.1.1 参数化单例
示例
using System;
using System.Collections.Concurrent;
using System.IO;namespace ParameterizedSingletonDemo
{// 日志级别枚举public enum LogLevel{Debug,Info,Warning,Error}// 参数化单例日志类public sealed class Logger{// 存储不同参数配置的单例实例private static readonly ConcurrentDictionary<string, Logger> _instances =new ConcurrentDictionary<string, Logger>();private readonly string _logFilePath;private readonly LogLevel _minLogLevel;private static readonly object _lock = new object();// 私有构造函数防止外部实例化private Logger(string logFilePath, LogLevel minLogLevel){_logFilePath = logFilePath;_minLogLevel = minLogLevel;// 确保日志目录存在var directory = Path.GetDirectoryName(logFilePath);if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory)){Directory.CreateDirectory(directory);}Console.WriteLine($"Logger initialized with path: {logFilePath} and level: {minLogLevel}");}// 获取单例实例的公共方法(参数化)public static Logger GetInstance(string logFilePath, LogLevel minLogLevel){// 使用文件路径和日志级别的组合作为键string key = $"{logFilePath}_{minLogLevel}";// 如果已有对应配置的实例,直接返回if (_instances.TryGetValue(key, out var existingInstance)){return existingInstance;}lock (_lock){// 双重检查锁定if (_instances.TryGetValue(key, out existingInstance)){return existingInstance;}// 创建新实例并添加到字典var newInstance = new Logger(logFilePath, minLogLevel);_instances[key] = newInstance;return newInstance;}}// 日志记录方法public void Log(LogLevel level, string message){if (level < _minLogLevel) return;var logEntry = $"{DateTime.Now:yyyy-MM-dd HH:mm:ss} [{level}] - {message}";lock (_lock){File.AppendAllText(_logFilePath, logEntry + Environment.NewLine);}Console.WriteLine(logEntry);}}class Program{static void Main(string[] args){Console.WriteLine("参数化单例模式演示 - 日志系统");Console.WriteLine("==================================");// 获取不同配置的日志实例var debugLogger = Logger.GetInstance("logs/debug.log", LogLevel.Debug);var errorLogger = Logger.GetInstance("logs/error.log", LogLevel.Error);// 尝试获取相同配置的实例 - 应该返回现有实例var debugLogger2 = Logger.GetInstance("logs/debug.log", LogLevel.Debug);// 验证确实是同一个实例Console.WriteLine($"两个Debug Logger是同一个实例: {ReferenceEquals(debugLogger, debugLogger2)}");// 记录一些日志debugLogger.Log(LogLevel.Debug, "这是一条调试信息");debugLogger.Log(LogLevel.Info, "这是一条普通信息");debugLogger.Log(LogLevel.Warning, "这是一条警告信息");debugLogger.Log(LogLevel.Error, "这是一条错误信息");errorLogger.Log(LogLevel.Debug, "这条调试信息不会被记录到error.log");errorLogger.Log(LogLevel.Error, "这是一条错误信息,会被记录");// 获取另一个配置的实例var infoLogger = Logger.GetInstance("logs/info.log", LogLevel.Info);infoLogger.Log(LogLevel.Debug, "这条调试信息不会被记录");infoLogger.Log(LogLevel.Info, "这是一条普通信息,会被记录");Console.WriteLine();Console.WriteLine("日志已记录完成,请查看logs目录下的文件");Console.WriteLine("按任意键退出...");Console.ReadKey();}}
}
输出
参数化单例模式演示 - 日志系统
==================================
Logger initialized with path: logs/debug.log and level: Debug
Logger initialized with path: logs/error.log and level: Error
两个Debug Logger是同一个实例: True
2025-09-21 16:12:23 [Debug] - 这是一条调试信息
2025-09-21 16:12:23 [Info] - 这是一条普通信息
2025-09-21 16:12:23 [Warning] - 这是一条警告信息
2025-09-21 16:12:23 [Error] - 这是一条错误信息
2025-09-21 16:12:23 [Error] - 这是一条错误信息,会被记录
Logger initialized with path: logs/info.log and level: Info
2025-09-21 16:12:23 [Info] - 这是一条普通信息,会被记录
说明
- 参数化单例:根据不同的初始化参数创建不同的单例实例
- 线程安全:使用双重检查锁定和ConcurrentDictionary确保线程安全
- 按需创建:只有在需要时才创建实例
- 配置管理:相同配置返回同一实例,不同配置创建新实例
3.1.2 支持子类化扩展
目标:
- 不修改业务代码,仅通过配置文件就能让
Singleton.Current
返回不同的子类实例。 - 仍保证全局唯一、线程安全、延迟加载。
实现要点:
- 把构造器
protected
化,允许子类化。 - 提供一个静态
Factory
委托,默认指向自己的构造器。 - 在静态构造函数里读取配置,替换
Factory
委托即可。
代码模板:
using System;
using System.Threading;public class Logger
{private static Logger _instance;private static readonly object _lock = new object();private static Func<Logger> _factory = () => new Logger();// 全局访问点public static Logger Current{get{if (_instance == null){lock (_lock){if (_instance == null){_instance = _factory();}}}return _instance;}}protected Logger() { }// 运行时切换实现public static void SetImplementation<T>() where T : Logger, new(){lock (_lock){_factory = () => new T();_instance = null; // 强制下次访问时重新创建实例}}public virtual void Write(string msg) => Console.WriteLine($"[Base] {msg}");
}// 子类
public class FileLogger : Logger
{public override void Write(string msg) => Console.WriteLine($"[File] {msg}");
}// 另一个子类示例
public class DatabaseLogger : Logger
{public override void Write(string msg) => Console.WriteLine($"[Database] {msg}");
}class Program
{static void Main(string[] args){// 程序启动时设置实现Logger.SetImplementation<FileLogger>();Logger.Current.Write("hello"); // -> [File] hello// 运行时动态切换实现Logger.SetImplementation<DatabaseLogger>();Logger.Current.Write("world"); // -> [Database] world// 测试多线程环境下的安全性Parallel.Invoke(() => Logger.Current.Write("Thread 1"),() => Logger.Current.Write("Thread 2"));}
}
[File] hello
[Database] world
[Database] Thread 1
[Database] Thread 2
优势:
- 支持继承、多态,突破静态类无法扩展的瓶颈。
3.1.3 多例模式
目标:
把“唯一”放松成“有限集合”,通过 Key 获取池内共享实例。
常用于数据库连接池、线程池、配置分段缓存等场景。
实现要点:
- 并发字典
ConcurrentDictionary
存储实例。 - 支持 Key 策略:枚举、字符串、甚至泛型。
- 允许预创建(饿汉)或延迟创建(懒汉)。
代码模板(泛型版,可复用):
public abstract class Multiton<TKey, TInstance>where TInstance : Multiton<TKey, TInstance>
{private static readonly ConcurrentDictionary<TKey, TInstance> _map = new();// 子类必须提供工厂protected abstract TInstance CreateInstance(TKey key);public static TInstance Get(TKey key){return _map.GetOrAdd(key, k =>{var inst = Activator.CreateInstance(typeof(TInstance), nonPublic: true) as TInstance;return inst!.CreateInstance(k);});}
}// 具体连接池示例
public sealed class ConnectionPool : Multiton<int, ConnectionPool>
{public string Id { get; } = Guid.NewGuid().ToString("N")[..6];// 保护构造器,防止外部 newprivate ConnectionPool() { }protected override ConnectionPool CreateInstance(int key) => new ConnectionPool();public void Execute(string sql) => Console.WriteLine($"[{Id}] execute: {sql}");
}
使用:
var pool0 = ConnectionPool.Get(0);
var pool1 = ConnectionPool.Get(1);
var pool0Again = ConnectionPool.Get(0);
Console.WriteLine(ReferenceEquals(pool0, pool0Again)); // True
扩展技巧:
- 限制总量:在
CreateInstance
中维护计数器,达到上限抛异常或复用。 - 过期策略:把 Value 包装成
WeakReference
,配合定期清理后台线程。 - 异步创建:把工厂改为
Func<TKey, Task<TInstance>>
,使用AsyncLazy<T>
。
3.1.4 泛型单例模板
系统中如存在多个需实现单例的类,逐个编写单例代码繁琐且重复。利用 CLR「泛型静态字段按闭合类型隔离」的特性,可实现一行代码安全获取任意类型的单例实例。
public sealed class Singleton<T> where T : class
{// 1. 创建一个静态的、只读的 Lazy<T> 实例private static readonly Lazy<T> _lazy = new(// 2. 在运行时根据类型信息(Type)来动态创建一个对象实例() => (T)Activator.CreateInstance(typeof(T), true)!);// 3. 这是获取单例实例的公共属性public static T Instance => _lazy.Value;
}
示例:
var cache = Singleton<MemoryCache>.Instance;
var converter = Singleton<JsonConverter>.Instance;
其核心依赖于 CLR 对泛型类型的处理机制:
-
闭合类型(Closed Constructed Type)
当使用具体类型(如
MemoryCache
)替换泛型参数T
时,即形成一个“闭合类型”。每个闭合类型在运行时都是独立的类型。 -
静态字段隔离
CLR 会为每个闭合类型分配独立的静态字段存储。因此
Singleton<MemoryCache>
和Singleton<JsonConverter>
拥有各自独立的_lazy
实例,从而实现单例隔离。
注意:
- 无法阻止外部
new T()
,适合内部组件,不适合库对外暴露。
3.1.5 与静态类对比
特性 | 静态类 | 单例(扩展后) |
---|---|---|
延迟初始化 | × | √(Lazy\<T>) |
支持接口/多态 | × | √ |
支持继承 | × | √ |
可替换实现 | × | √ |
支持 Dispose | × | √ |
支持计数/池化 | × | √(Multiton) |
3.2 其他
3.2.1 异步单例
示例
using AsyncSingletonDemo;
using System;
using System.Net.Http;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;Console.WriteLine("=== 异步单例 Demo ===");// 模拟多个线程同时需要配置
var tasks = new Task[5];
for (int i = 0; i < tasks.Length; i++)
{string threadName = $"Thread-{i + 1}";tasks[i] = Task.Run(() => PrintConfigAsync(threadName));
}await Task.WhenAll(tasks);static async Task PrintConfigAsync(string tag)
{// 第一次访问时才会真正去远程拉一次var cfg = await ConfigService.Instance.GetAsync();Console.WriteLine($"[{DateTime.Now:HH:mm:ss.fff}] {tag} 拿到配置,长度:{cfg.Length}");
}namespace AsyncSingletonDemo
{/// <summary>/// 异步单例:远程配置服务/// </summary>public sealed class ConfigService{// 单例实例public static ConfigService Instance { get; } = new ConfigService();// 缓存配置任务private readonly Lazy<Task<string>> _configTask;private ConfigService(){_configTask = new Lazy<Task<string>>(FetchRemoteConfigAsync,System.Threading.LazyThreadSafetyMode.ExecutionAndPublication);}// 公开接口:返回配置任务public Task<string> GetAsync() => _configTask.Value;// 真正的异步初始化逻辑private static async Task<string> FetchRemoteConfigAsync(){Console.WriteLine($"[{DateTime.Now:HH:mm:ss.fff}] 正在远程拉取配置...");// 模拟网络延迟await Task.Delay(1000);using var http = new HttpClient();try{// 使用更可靠的测试APIvar json = await http.GetStringAsync("https://jsonplaceholder.typicode.com/posts/1");Console.WriteLine($"[{DateTime.Now:HH:mm:ss.fff}] 配置拉取完成");return json;}catch (HttpRequestException ex){Console.WriteLine($"[{DateTime.Now:HH:mm:ss.fff}] 配置拉取失败: {ex.Message}");return "{}"; // 返回空配置作为降级方案}}}
}
说明
- 使用
Lazy<Task<T>>
结合LazyThreadSafetyMode.ExecutionAndPublication
确保初始化代码只执行一次
- 配置获取后会被缓存,后续调用直接返回已缓存的结果
- 避免了不必要的重复网络请求
输出
=== 异步单例 Demo ===
[16:10:32.658] 正在远程拉取配置...
[16:10:34.772] 配置拉取完成
[16:10:34.776] Thread-4 拿到配置,长度:292
[16:10:34.776] Thread-3 拿到配置,长度:292
[16:10:34.776] Thread-5 拿到配置,长度:292
[16:10:34.776] Thread-2 拿到配置,长度:292
[16:10:34.776] Thread-1 拿到配置,长度:292
3.2.2 依赖注入友好的单例
传统单例模式需要开发者自行处理线程安全、延迟初始化等复杂问题,而依赖注入友好的方式则是将对象生命周期管理职责完全交给DI容器。
通过简单调用 services.AddSingleton<TInterface, TImpl>()
,容器会自动处理:
- 单例保障 - 确保整个应用生命周期内只创建一个实例
- 依赖解析 - 自动注入所有必需的依赖项(如示例中的
ILogger
) - 线程安全 - 容器负责保证实例化过程的线程安全性
- 可测试性 - 支持在单元测试中轻松替换为Mock实现
这种方式的核心优势是关注点分离:类只需声明其依赖关系,而不需要关心自身生命周期管理。
3.2.3 示例
// 定义清晰的服务接口
public interface ISingletonService
{void ExecuteOperation();
}// 实现类只关注业务逻辑,不管理生命周期
public class SingletonService : ISingletonService
{private readonly ILogger<SingletonService> _logger;// 通过构造函数明确声明依赖需求public SingletonService(ILogger<SingletonService> logger){_logger = logger;}public void ExecuteOperation(){_logger.LogInformation("操作已执行");// 业务逻辑实现}
}// 在启动时注册服务(通常在Startup.cs或Program.cs中)
// services.AddSingleton<ISingletonService, SingletonService>();
3.2.4 最佳实践建议
- 面向接口编程 - 始终依赖抽象而非具体实现
- 明确依赖 - 通过构造函数清晰声明所有依赖项
- 避免静态状态 - 确保单例类无状态或线程安全的状态管理
- 注意依赖链 - 确保单例服务的所有依赖项也具有兼容的生命周期
这种模式在现代应用开发中已成为首选方案,特别适合ASP.NET Core、微服务架构等场景。
3.2.5 线程级单例(ThreadLocal)
可参考我的另一篇博客:.NET 线程本地存储 (TLS,Thread Local Storage)|ThreadStatic、ThreadLocal<T>、AsyncLocal<T>-CSDN博客
每个线程拿到各自唯一实例,实现“线程内单例、线程间多例”,如 SimpleDateFormat 的线程封闭。
3.2.6 集群/分布式单例
在微服务和分布式架构中,常常需要确保某个关键任务或服务在整个集群的多个节点中只有一个实例在运行。这超越了传统单例模式的范畴,引入了分布式协调的问题。
实现集群级单例通常需要借助外部系统,如:
- 分布式锁:使用 Redis(如 RedLock 算法)或 ZooKeeper 来争夺一个全局锁,获得锁的节点成为“主节点”。
- 领导者选举:利用 ZooKeeper、Etcd 等协调服务的内置选举机制来指定主节点。
- 数据库唯一约束:通过向数据库插入一条唯一记录来竞争执行权。
由于其实现涉及分布式系统的诸多复杂性问题(如网络分区、脑裂、锁的续约与释放等),这部分内容相对独立且复杂,我们将在后续的文章中继续探索,以免打断本文对单例模式核心概念的讲解。
4 破坏单例的场景及防御
单例模式实现时除了注意线程安全初始化外还需要防止外部破坏。
4.1 破坏单例的场景及防御措施
4.1.2 反射破坏
1 反射攻击原理与示例
.NET 的反射 (System.Reflection
)具有极高权限,可绕过语言层面的访问限制,直接调用私有构造函数,从而创建多个实例,破坏单例的唯一性。
- 获取私有构造函数信息(使用
BindingFlags.NonPublic
)。 - 通过
Invoke
方法强制调用构造函数创建新实例。
示例
using System;
using System.Reflection;namespace ReflectionAttackDemo
{// 单例类public sealed class Singleton{private static readonly Singleton _instance = new Singleton();// 私有构造函数private Singleton(){Console.WriteLine("Singleton constructor called");}public static Singleton Instance => _instance;public void ShowMessage(string message){Console.WriteLine($"Singleton says: {message}");}}class Program{static void Main(string[] args){Console.WriteLine("=== 反射攻击演示 ===\n");// 获取合法单例实例var legalInstance = Singleton.Instance;legalInstance.ShowMessage("我是合法实例");Console.WriteLine("\n开始反射攻击...\n");// 反射攻击代码var type = typeof(Singleton);var constructor = type.GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance,null,Type.EmptyTypes, // 获取无参构造函数null);// 通过反射创建非法实例var illegalInstance = constructor.Invoke(null) as Singleton;illegalInstance.ShowMessage("我是反射创建的非法实例");// 输出不同哈希码,证明单例被破坏Console.WriteLine($"\n哈希码对比:");Console.WriteLine($"合法实例: {legalInstance.GetHashCode()}");Console.WriteLine($"非法实例: {illegalInstance.GetHashCode()}");Console.WriteLine($"\n实例相等性检查: {ReferenceEquals(legalInstance, illegalInstance)}");Console.WriteLine($"单例模式已被反射破坏: {!ReferenceEquals(legalInstance, illegalInstance)}");Console.WriteLine("\n=== 演示结束 ===");Console.ReadKey();}}
}
说明
=== 反射攻击演示 ===Singleton constructor called
Singleton says: 我是合法实例开始反射攻击...Singleton constructor called
Singleton says: 我是反射创建的非法实例哈希码对比:
合法实例: 27252167
非法实例: 59941933实例相等性检查: False
单例模式已被反射破坏: True=== 演示结束 ===
2 防御方案
- 静态标志位防御
3 基础防御方案:静态标志位检查
通过静态字段跟踪实例化状态,并在构造函数中验证:
private static bool _created = false; // 静态标志位
private Singleton()
{if (_created)throw new InvalidOperationException("Singleton instance already exists.");_created = true; // 标记为已创建
}
原理:
无论通过正常方式或反射调用构造函数,均需经过标志位检查。首次调用后 _created
被设为 true
,后续调用将抛出异常。
关键要求:
- 静态字段:标志位必须是
static
,以确保所有实例尝试共享同一状态。 - 线程安全:基础布尔值版本存在并发漏洞,需加固。
4 线程安全加固方案
使用原子操作(如 Interlocked
)避免多线程竞争:
private static int _creationFlag = 0; // 0表示未创建,1表示已创建
private Singleton()
{// 原子检查并标记:若当前值为0,则替换为1;若替换失败(已为1),则抛出异常if (Interlocked.CompareExchange(ref _creationFlag, 1, 0) != 0){throw new InvalidOperationException("Singleton instance already exists.");}
}
优势:
Interlocked.CompareExchange
是原子操作,可彻底解决多线程下的重复实例化问题。
5 局限性
静态标志位仍可能被反射绕过:攻击者可重置 _created
字段的值后再次调用构造函数。
// 反射重置标志位并再次攻击
var field = typeof(Singleton).GetField("_created", BindingFlags.NonPublic | BindingFlags.Static);
field.SetValue(null, false); // 强制重置为false
var constructor = ... // 获取构造函数
var newInstance = constructor.Invoke(null); // 成功创建新实例
结论:标志位方案仅能阻止简单反射攻击,无法抵御有意修改内部状态的恶意行为。
-
实例引用检查
通过比较静态实例引用增强安全性(需配合饿汉式初始化):
public sealed class Singleton {private static readonly Singleton _instance = new Singleton();public static Singleton Instance => _instance;private Singleton(){// 检查当前构造的对象是否与静态实例一致if (_instance != null && !ReferenceEquals(this, _instance)){throw new InvalidOperationException("Singleton instance already exists.");}} }
优势:
攻击者难以伪造现有实例引用,防御性更强。需注意此方案适用于饿汉式初始化,懒汉式需调整逻辑。
4.1.2 序列化/反序列化破坏
1 破坏原理
标准的.NET序列化/反序列化机制会绕开类的构造函数(包括私有构造函数),通过FormatterServices.GetUninitializedObject
方法在反序列化时为对象分配内存,然后逐一填充被序列化的字段数据。
这个过程完全创建了一个新的对象实例,从而破坏了单例的唯一性。
2 防御方案
首要建议:避免使用不安全的序列化器
最重要的防御是:不要使用 BinaryFormatter
。
微软已正式将 BinaryFormatter
标记为过时(obsolete),并强烈建议不要使用它,原因正是其安全性漏洞(如反序列化攻击)和设计上的问题。在新的项目中,应优先选择更安全、更现代的序列化方案。
首选方案:使用安全的现代序列化器
对于配置、日志、数据传输等场景,请使用以下安全且不支持这种破坏机制的序列化器:
System.Text.Json
(.NET Core 3.0+)Newtonsoft.Json
(Json.NET)XmlSerializer
DataContractSerializer
这些序列化器在反序列化时通常会调用类的构造函数(包括公共或通过特性指定的构造函数)。因此,如果你的单例实例通过静态属性或字段提供,并且构造函数是私有的,这些序列化器根本无法创建新实例,通常会抛出异常,从而从根本上杜绝了被破坏的可能。
如果单例必须被序列化/反序列化,正确的做法是让序列化器只序列化单例的数据内容,而不是其身份(Identity)。在反序列化时,将数据应用到现有的单例实例上,而不是创建一个新的。
4.1.3 克隆破坏
- ** 问题分析**
若单例类实现了 ICloneable
接口,则可能通过 Clone()
方法创建对象的副本,从而破坏单例的唯一性。
- 防御方案
避免实现 ICloneable
接口
单例类应从根本上避免实现可能破坏唯一性的接口,除非有明确需求。通过不实现 ICloneable
,可杜绝通过克隆创建副本的途径。
显式禁用克隆行为
若必须实现 ICloneable
(例如接口约束或继承要求),需在 Clone()
方法中显式抛出异常,禁止外部调用:
public object Clone() => throw new NotSupportedException("Singleton instances cannot be cloned.");
- 补充防御措施
- 密封类(Sealed):防止通过派生类绕过单例限制。
- 注释警示:在代码中添加明确说明,提示开发者勿修改单例的唯一性设计。
- 代码审查:确保团队遵循单例模式的最佳实践,避免误实现克隆逻辑。
4.1.4 最佳实践:依赖注入容器
在现代应用开发中,依赖注入(DI)容器是管理单例生命周期的首选方案。容器负责保证实例唯一性,无需开发者手动防御反射攻击:
// 在启动类中注册服务为单例
services.AddSingleton<ISingletonService, SingletonService>();
优势:
- 容器自动控制实例创建与生命周期。
- 彻底避免反射、序列化等问题,提升代码安全性和可维护性。
4.1.3 避免滥用单例(全局变量陷阱)
- 单例常被误用为全局变量,可能会导致一下问题
-
可测试性差:难以模拟(Mock)和隔离测试。
-
隐式依赖:违反依赖倒置原则(DIP)。
调用方无需显式声明依赖,导致代码耦合度高且难以重构。
-
状态残留:测试间状态污染。
测试用例之间相互干扰,需额外清理状态,增加测试复杂度。
-
资源占用:饿汉式单例可能导致不必要的初始化开销。
-
- 解决方案
-
评估必要性:仅在逻辑上确实需要唯一实例时使用单例。
-
依赖注入(DI):通过容器管理单例生命周期(如 ASP.NET Core):
builder.Services.AddSingleton<IMyService, MyService>();
-
工厂模式:封装对象创建逻辑,可控实例化。
-
4.2 总结
- 优先DI容器:绝大多数场景通过DI管理单例,避免手动实现。
- 严控适用场景:仅限“共享状态+资源昂贵+协调逻辑”。
- 保证线程安全与生命周期可控:懒加载、资源释放、防御性编程。
- 为测试而设计:接口抽象 + 依赖注入,避免隐式耦合。
5 相关模式
单例模式常与其他创建型模式结合使用,但各有侧重:
- 与工厂方法/抽象工厂结合:单例常作为全局唯一的工厂实例,确保整个应用使用同一工厂配置,同时封装对象创建逻辑。
- 与建造者模式结合:单例可管理全局唯一的建造者配置或缓存,保证复杂对象的构建过程的一致性。
- 与原型模式结合:单例可维护全局原型注册表,提供唯一访问点来克隆预定义原型对象。
- 与对象池模式结合:单例可管理全局资源池(如数据库连接池),确保资源的集中分配和回收。
此外,单例也常与结构型模式(如门面模式、代理模式)或行为型模式(如状态模式、策略模式)结合,作为全局访问点统一管理核心组件或状态。
需注意避免过度使用单例,防止全局状态污染和代码耦合。
6 总结
单例模式通过私有构造、静态字段和全局访问点三要素,确保类在生命周期内唯一实例,实现资源节约、状态一致和全局协调。
其实现需在懒汉/饿汉式、线程安全与性能间权衡,现代开发中推荐使用Lazy<T>或依赖注入容器。
高级场景可扩展为参数化、多例或泛型单例。需警惕反射、序列化等破坏手段,并通过标志位检查、避免危险API或交由容器管理来防御。
切忌滥用单例为全局变量,应遵循“面向接口、依赖注入”原则,保证代码可测试与可维护性。