单例设计模式是创建型设计模式 的核心之一,核心目标是:保证一个类在整个应用程序生命周期中,只有一个实例对象,并提供一个全局唯一的访问点------ 简单说,就是 "一个类只能 new 一次,所有地方用的都是同一个对象"。
一、什么是单例设计模式?
1. 官方定义
单例模式确保一个类仅有一个实例,并提供一个访问该实例的全局访问点。
2. 通俗比喻
单例模式就像:
- 校园二手平台的 "支付中心":整个平台只能有一个支付中心(多实例会导致支付状态混乱、重复扣款),所有订单的支付请求都必须通过这一个中心处理;
- 你手机的 "设置" 应用:无论你从桌面点多少次 "设置",打开的都是同一个设置界面(同一个实例),而非每次新建一个;
- 对比普通类:
new Goods()每次都会创建新对象,而单例类new一次后,后续调用都返回同一个对象。
3. 单例模式的 3 个核心原则(缺一不可)
| 核心原则 | 目的 |
|---|---|
| 私有构造函数 | 禁止外部通过new创建实例(private Singleton()),从根源控制实例数量; |
| 静态私有实例 | 存储类的唯一实例(private static Singleton _instance;),保证全局唯一; |
| 公共静态访问点 | 提供全局获取实例的方法(public static Singleton GetInstance()),外部只能通过该方法获取实例; |
二、单例模式的常见实现方式(从基础到最优)
单例的实现有多种方式,核心差异在于实例初始化时机 和线程安全,下面按 "易理解→高性能→线程安全" 的顺序讲解。
1. 饿汉式(Eager Singleton)------ 最简单,天生线程安全
核心逻辑
类加载时就创建实例("饿汉":迫不及待初始化),无需考虑线程安全,适合实例占用内存小、启动时必须初始化的场景。
代码实现(缓存管理器示例)
cs
/// <summary>
/// 饿汉式单例:缓存管理器(校园二手平台统一缓存商品数据)
/// </summary>
public class CacheManager
{
// 1. 静态私有实例:类加载时直接初始化(唯一实例)
private static readonly CacheManager _instance = new CacheManager();
// 2. 私有构造函数:禁止外部new
private CacheManager()
{
// 初始化缓存(比如连接Redis)
Console.WriteLine("CacheManager实例已创建(饿汉式)");
}
// 3. 公共静态访问点:全局获取实例
public static CacheManager GetInstance()
{
return _instance;
}
// 业务方法:添加缓存
public void SetCache(string key, object value)
{
Console.WriteLine($"添加缓存:{key} = {value}");
}
// 业务方法:获取缓存
public object GetCache(string key)
{
Console.WriteLine($"获取缓存:{key}");
return $"缓存值-{key}";
}
}
调用示例
cs
// 多次调用GetInstance,返回同一个实例
var cache1 = CacheManager.GetInstance();
var cache2 = CacheManager.GetInstance();
Console.WriteLine(cache1 == cache2); // 输出:True(同一个对象)
cache1.SetCache("hot_goods", "二手笔记本、二手手机");
cache2.GetCache("hot_goods"); // 输出:获取缓存:hot_goods
优缺点
| 优点 | 缺点 |
|---|---|
| 实现简单,一行代码初始化; | 类加载时就创建实例,若实例未被使用则浪费内存; |
| 天生线程安全(CLR 保证静态字段初始化线程安全); | 无法延迟初始化(比如启动时不需要缓存,却提前创建); |
| 无锁,性能最优; | 不支持参数化初始化(比如动态配置 Redis 连接); |
适用场景
- 实例占用内存极小(如日志管理器、配置管理器);
- 应用启动时必须初始化的核心组件(如支付中心)。
2. 懒汉式(Lazy Singleton)------ 延迟初始化(基础版,线程不安全)
核心逻辑
第一次调用访问点时才创建实例("懒汉":不到用的时候不初始化),节省内存,但基础版存在线程安全问题。
代码实现
cs
/// <summary>
/// 懒汉式单例(基础版:线程不安全)
/// </summary>
public class LazyCacheManager
{
// 1. 静态私有实例:初始为null,延迟初始化
private static LazyCacheManager _instance = null;
// 2. 私有构造函数
private LazyCacheManager()
{
Console.WriteLine("LazyCacheManager实例已创建(懒汉式)");
}
// 3. 公共静态访问点:第一次调用时创建实例
public static LazyCacheManager GetInstance()
{
if (_instance == null) // 线程不安全:多线程同时进入会创建多个实例
{
_instance = new LazyCacheManager();
}
return _instance;
}
// 业务方法(同饿汉式)
public void SetCache(string key, object value) => Console.WriteLine($"添加缓存:{key} = {value}");
}
线程安全问题演示
cs
// 多线程调用,会创建多个实例(单例失效)
Parallel.For(0, 10, i =>
{
var cache = LazyCacheManager.GetInstance();
Console.WriteLine($"线程{i}:{cache.GetHashCode()}");
});
// 输出:多个不同的HashCode(说明创建了多个实例)
优缺点
| 优点 | 缺点 |
|---|---|
| 延迟初始化,节省内存; | 多线程环境下会创建多个实例,破坏单例; |
| 支持参数化初始化(扩展); | 无锁,单线程性能优,但多线程不可用; |
适用场景
仅适用于单线程环境(如桌面小工具),生产环境(多线程 Web 应用)绝对不能用!
3. 懒汉式(线程安全版)------ 加锁(Lock)
核心逻辑
在基础懒汉式上添加lock锁,保证多线程下只有一个线程能创建实例,解决线程安全问题。
代码实现
cs
/// <summary>
/// 懒汉式单例(线程安全版:加Lock)
/// </summary>
public class ThreadSafeCacheManager
{
// 1. 静态私有实例
private static ThreadSafeCacheManager _instance = null;
// 2. 锁对象:必须是静态私有只读(避免外部修改)
private static readonly object _lockObj = new object();
// 3. 私有构造函数
private ThreadSafeCacheManager()
{
Console.WriteLine("ThreadSafeCacheManager实例已创建(加锁版)");
}
// 4. 公共静态访问点:加锁保证线程安全
public static ThreadSafeCacheManager GetInstance()
{
lock (_lockObj) // 加锁:同一时间只有一个线程能进入
{
if (_instance == null)
{
_instance = new ThreadSafeCacheManager();
}
}
return _instance;
}
// 业务方法
public void SetCache(string key, object value) => Console.WriteLine($"添加缓存:{key} = {value}");
}
优缺点
| 优点 | 缺点 |
|---|---|
| 线程安全,多线程环境可用; | 每次调用 GetInstance 都要加锁,性能损耗(即使实例已创建); |
| 延迟初始化,节省内存; | 锁竞争激烈时(高频调用),性能下降; |
适用场景
中小并发场景(如校园二手平台的日志管理器),高频调用场景需优化。
4. 双重检查锁定(Double-Checked Locking)------ 最优懒汉式
核心逻辑
在加锁前先检查实例是否已创建(第一次检查),加锁后再检查一次(第二次检查),既保证线程安全,又避免频繁加锁,是生产环境最常用的单例实现。
代码实现(.NET 推荐写法)
cs
/// <summary>
/// 懒汉式单例(最优版:双重检查锁定)
/// </summary>
public class DoubleCheckCacheManager
{
// 1. 静态私有实例:加volatile关键字,禁止指令重排序(关键!)
private static volatile DoubleCheckCacheManager _instance = null;
// 2. 锁对象
private static readonly object _lockObj = new object();
// 3. 私有构造函数
private DoubleCheckCacheManager()
{
Console.WriteLine("DoubleCheckCacheManager实例已创建(双重检查)");
}
// 4. 公共静态访问点:双重检查+锁
public static DoubleCheckCacheManager GetInstance()
{
// 第一次检查:实例已创建则直接返回,避免加锁(99%的场景走这里)
if (_instance == null)
{
lock (_lockObj) // 仅实例未创建时加锁
{
// 第二次检查:防止多线程同时通过第一次检查后,重复创建实例
if (_instance == null)
{
_instance = new DoubleCheckCacheManager();
}
}
}
return _instance;
}
// 业务方法
public void SetCache(string key, object value) => Console.WriteLine($"添加缓存:{key} = {value}");
}
关键:volatile 关键字
volatile 禁止 CLR 对字段进行 "指令重排序"------new DoubleCheckCacheManager() 实际分三步:
- 分配内存;
- 初始化实例;
- 把
_instance指向内存地址。若没有volatile,CLR 可能重排序为 1→3→2,导致其他线程拿到 "未初始化的实例"(空引用异常)。
优缺点
| 优点 | 缺点 |
|---|---|
| 线程安全,多线程环境最优; | 代码稍复杂(需注意 volatile); |
| 延迟初始化,节省内存; | 无(生产环境首选); |
| 仅实例创建时加锁,高频调用性能无损耗; |
适用场景
生产环境所有多线程场景(校园二手平台的缓存管理器、支付中心、Redis 连接池),是.NET 中最推荐的单例实现。
5. 静态内部类(Lazy Initialization Holder Class)------ 极简最优版
核心逻辑
利用.NET "静态内部类只有在被引用时才加载" 的特性,实现延迟初始化,且天生线程安全(CLR 保证静态字段初始化线程安全)。
代码实现
cs
/// <summary>
/// 静态内部类单例(极简最优版)
/// </summary>
public class StaticInnerCacheManager
{
// 1. 私有构造函数
private StaticInnerCacheManager()
{
Console.WriteLine("StaticInnerCacheManager实例已创建(静态内部类)");
}
// 2. 静态内部类:仅在GetInstance调用时加载
private static class SingletonHolder
{
// 静态字段:类加载时初始化,CLR保证线程安全
internal static readonly StaticInnerCacheManager Instance = new StaticInnerCacheManager();
}
// 3. 公共静态访问点
public static StaticInnerCacheManager GetInstance()
{
return SingletonHolder.Instance;
}
// 业务方法
public void SetCache(string key, object value) => Console.WriteLine($"添加缓存:{key} = {value}");
}
优缺点
| 优点 | 缺点 |
|---|---|
| 线程安全,CLR 原生保证; | 无法实现参数化初始化; |
| 延迟初始化,节省内存; | |
| 无锁,性能最优; | |
| 代码极简,无 volatile / 锁等易错点; |
适用场景
无需参数化初始化的单例(如日志管理器、配置管理器),是 "无参数单例" 的最优解。
三、为什么要用单例模式?
单例模式的核心价值是控制实例数量,保证资源 / 状态统一,以下场景必须用单例:
1. 资源独占型组件(避免重复创建浪费资源)
- 缓存管理器:整个平台只有一个缓存实例,避免多实例重复缓存数据(如 Redis 连接池,多实例会导致连接数爆炸);
- 数据库连接池:单例管理数据库连接,避免频繁创建 / 销毁连接,提升性能;
- 文件管理器:单例管理文件读写,避免多实例同时写文件导致冲突。
2. 状态统一型组件(避免多实例状态不一致)
- 支付中心:整个平台只有一个支付实例,保证订单支付状态统一(多实例会导致重复扣款、支付状态混乱);
- 用户会话管理器:单例管理用户登录会话,避免多实例导致会话失效;
- 计数器:单例统计平台商品访问量,保证计数准确(多实例会导致计数重复 / 丢失)。
3. 全局工具类(避免多实例占用内存)
- 日志管理器:单例统一写日志到文件 / 数据库,避免多实例写日志时的 IO 冲突;
- 配置管理器 :单例加载
appsettings.json配置,避免多实例重复读取配置文件。
四、单例模式的注意事项(避坑指南)
单例模式虽好用,但有多个易踩的坑,必须注意:
1. 反射会破坏单例
反射可以通过ConstructorInfo调用私有构造函数,创建新实例:
cs
Type type = typeof(DoubleCheckCacheManager);
ConstructorInfo ctor = type.GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, null, Type.EmptyTypes, null);
var newInstance = (DoubleCheckCacheManager)ctor.Invoke(null);
Console.WriteLine(newInstance == DoubleCheckCacheManager.GetInstance()); // 输出:False(单例失效)
防护方案:在私有构造函数中加校验,若实例已存在则抛异常:
cs
private DoubleCheckCacheManager()
{
if (_instance != null)
{
throw new InvalidOperationException("单例类不允许创建多个实例");
}
Console.WriteLine("DoubleCheckCacheManager实例已创建");
}
2. 序列化会破坏单例
若单例类实现ISerializable,序列化 + 反序列化会创建新实例:防护方案 :重写GetObjectData方法,返回已有实例:
cs
public void GetObjectData(SerializationInfo info, StreamingContext context)
{
info.SetType(typeof(DoubleCheckCacheManager));
}
// 反序列化时返回单例实例
public static DoubleCheckCacheManager GetDeserializedInstance(SerializationInfo info, StreamingContext context)
{
return GetInstance();
}
3. 单例类无法被继承(构造函数私有)
若需扩展单例类,可改用 "单例 + 接口" 或 "工厂模式 + 单例"。
4. 单例类的状态是全局的,需注意线程安全
若单例类有可读写的成员变量(如public int Counter),多线程读写时需加锁:
cs
private int _counter = 0;
public int Counter
{
get { lock (_lockObj) return _counter; }
set { lock (_lockObj) _counter = value; }
}
五、总结:单例模式核心要点
-
核心目标:一个类只有一个实例,全局唯一访问点;
-
实现关键:私有构造函数 + 静态实例 + 全局访问点;
-
推荐实现:
- 无参数、需延迟初始化:静态内部类;
- 有参数 / 高频调用:双重检查锁定;
- 简单场景 / 启动必初始化:饿汉式;
-
适用场景:资源独占、状态统一、全局工具类;
-
避坑重点:线程安全、反射 / 序列化破坏单例、全局状态的线程安全。