单例设计模式(Singleton Pattern) 是一种创建型设计模式,它的核心目标是:
确保一个类在整个应用程序生命周期中,有且仅有一个实例,并提供一个全局访问点。
✅ 为什么需要单例?
有些对象天然只需要一个,比如:
- 日志记录器(Logger)
- 配置管理器(ConfigManager)
- 数据库连接池
- 线程池
- 应用程序的主窗口(某些桌面应用)
如果允许多个实例:
- 可能导致资源浪费(如重复读取配置文件)
- 引发状态不一致(如多个 Logger 写同一个文件冲突)
- 违反业务逻辑(如"系统设置"只能有一份)
👉 单例就是为了解决这些问题。
✅ 单例的核心要素
要实现真正的单例,必须满足 3 个条件:
- 私有构造函数 → 防止外部
new - 静态私有字段 → 保存唯一实例
- 公共静态属性/方法 → 提供全局访问点
✅ C# 中的单例实现方式(从简单到线程安全)
方式 1️⃣:懒汉式(Lazy,非线程安全)------ ❌ 不推荐
public class Singleton
{
private static Singleton _instance;
private Singleton() { } // 私有构造函数
public static Singleton Instance
{
get
{
if (_instance == null)
_instance = new Singleton();
return _instance;
}
}
}
⚠️ 问题:多线程下可能创建多个实例!
方式 2️⃣:饿汉式(Eager)------ ✅ 简单安全,但可能浪费资源
public class Singleton
{
private static readonly Singleton _instance = new Singleton();
private Singleton() { }
public static Singleton Instance => _instance;
}
✅ 优点 :线程安全(静态字段在类型加载时初始化)
❌ 缺点:即使不用也会创建实例(不支持懒加载)
方式 3️⃣:双重检查锁(Double-Checked Locking)------ ✅ 经典线程安全
public class Singleton
{
private static volatile Singleton _instance;
private static readonly object _lock = new object();
private Singleton() { }
public static Singleton Instance
{
get
{
if (_instance == null) // 第一次检查
{
lock (_lock)
{
if (_instance == null) // 第二次检查
_instance = new Singleton();
}
}
return _instance;
}
}
}
✅ 优点 :线程安全 + 懒加载
⚠️ 注意 :必须加 volatile 防止指令重排序(.NET 中有时可省略,但建议保留)
方式 4️⃣:推荐!使用 Lazy<T>(C# 4+)------ ✅ 最简洁安全
public class Singleton
{
private static readonly Lazy<Singleton> _instance =
new Lazy<Singleton>(() => new Singleton());
private Singleton() { }
public static Singleton Instance => _instance.Value;
}
✅ 优点:
- 线程安全(
Lazy<T>内部已处理) - 懒加载
- 代码简洁
- 支持异常处理和自定义初始化逻辑
⚠️ 单例的常见陷阱
| 问题 | 说明 |
|---|---|
| 反射破坏 | 通过反射可调用私有构造函数 → 可加 if (_instance != null) throw 防御 |
| 序列化破坏 | 反序列化可能创建新实例 → 实现 ISerializable 或使用 [NonSerialized] |
| 多线程安全 | 必须确保初始化过程线程安全(见上文) |
| 单元测试困难 | 全局状态难以 mock → 可考虑依赖注入替代 |
🆚 单例 vs 静态类
| 特性 | 单例 | 静态类 |
|---|---|---|
| 可继承/实现接口 | ✅ 可以 | ❌ 不可以 |
| 延迟初始化 | ✅ 可以 | ❌ 静态字段在类型加载时初始化 |
| 支持多态 | ✅ 可以 | ❌ 不行 |
| 单元测试友好度 | 中等(可通过接口解耦) | 差(无法 mock) |
| 内存位置 | 托管堆 | 静态区 |
💡 如果你只需要工具方法(无状态),用静态类 ;
如果你需要状态 + 全局唯一 + 可扩展 ,用单例。
✅ 现代替代方案:依赖注入(DI)
在 ASP.NET Core 等现代框架中,更推荐用 DI 容器注册单例,而非手写单例:
// 注册
services.AddSingleton<ILogger, FileLogger>();
// 使用(通过构造函数注入)
public class MyService
{
private readonly ILogger _logger;
public MyService(ILogger logger) => _logger = logger;
}
✅ 优势:
- 解耦
- 易于测试
- 生命周期由容器管理
- 避免全局状态污染
📌 建议:在新项目中优先考虑 DI,仅在无法使用 DI 的场景(如旧系统、工具类库)使用传统单例。
✅ 小结
| 关键点 | 说明 |
|---|---|
| 目的 | 确保全局只有一个实例 |
| 三要素 | 私有构造函数 + 静态实例 + 全局访问点 |
| 推荐实现 | Lazy<T> 或 DI 容器 |
| 慎用场景 | 避免滥用(不是所有"全局"都需要单例) |
🧠 一句话记住 :
"单例 = 全局唯一实例,懒加载 + 线程安全是关键。"
问题
什么是全局状态
它是可以从应用程序任何位置访问的状态
为什么单例是一种反模式
因为它是一部分全局状态,而全局状态很难控制。
单例设计模式和应用程序单例有什么区别
应用程序单例只是在应用程序各处使用的单一对象,他不会强制其单例性,而单例设计模式会。