.NET进阶——设计模式(1)单例设计模式

单例设计模式是创建型设计模式 的核心之一,核心目标是:保证一个类在整个应用程序生命周期中,只有一个实例对象,并提供一个全局唯一的访问点------ 简单说,就是 "一个类只能 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() 实际分三步:

  1. 分配内存;
  2. 初始化实例;
  3. _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; }
}

五、总结:单例模式核心要点

  1. 核心目标:一个类只有一个实例,全局唯一访问点;

  2. 实现关键:私有构造函数 + 静态实例 + 全局访问点;

  3. 推荐实现

    • 无参数、需延迟初始化:静态内部类;
    • 有参数 / 高频调用:双重检查锁定;
    • 简单场景 / 启动必初始化:饿汉式;
  4. 适用场景:资源独占、状态统一、全局工具类;

  5. 避坑重点:线程安全、反射 / 序列化破坏单例、全局状态的线程安全。

相关推荐
切糕师学AI3 小时前
.NET 如何引用两个不同版本的dll?
c#·.net
用户4488466710607 小时前
.NET进阶——深入理解泛型(4)泛型的协变逆变
.net
步步为营DotNet8 小时前
深度解析.NET中HttpClient的生命周期管理:构建稳健高效的HTTP客户端
网络协议·http·.net
缺点内向9 小时前
如何在 C# 中高效的将 XML 转换为 PDF
xml·后端·pdf·c#·.net
时光追逐者9 小时前
Visual Studio 2026 正式版下载与安装详细教程!
ide·c#·.net·.net core·visual studio
唐青枫10 小时前
C# 列表模式(List Patterns)深度解析:模式匹配再进化!
c#·.net
步步为营DotNet1 天前
深入理解IAsyncEnumerable:.NET中的异步迭代利器
服务器·前端·.net
玩泥巴的1 天前
强的飞起的 Roslyn 编译时代码生成,实现抽象类继承与依赖注入的自动化配置
c#·.net·代码生成·roslyn
mudtools1 天前
强的飞起的 Roslyn 编译时代码生成,实现抽象类继承与依赖注入的自动化配置
c#·.net