C# 设计模式——单例模式

在C#中,单例设计模式(Singleton Pattern) 是一种创建型设计模式,核心目标是确保一个类在整个应用程序生命周期中只存在一个实例,并提供一个全局访问点供外部使用。这种模式适合管理共享资源(如配置文件、日志记录器、数据库连接池等),避免频繁创建实例导致的资源浪费或状态不一致。

单例模式的核心要素

要实现单例模式,必须满足以下3个条件:

  1. 私有构造函数 :阻止外部通过new关键字创建实例。
  2. 静态私有实例:在类内部维护唯一的实例对象。
  3. 静态公共访问点:提供一个全局方法/属性,返回该唯一实例。

一、常见实现方式(从简单到线程安全)

单例模式的实现难点在于多线程环境下的线程安全 (避免并发创建多个实例)和延迟初始化(按需创建实例,节省资源)。以下是几种典型实现:

1. 饿汉式(Eager Initialization)

特点:类加载时立即初始化实例(非延迟加载),天然线程安全(C#中静态构造函数由CLR保证线程安全)。

csharp 复制代码
public sealed class SingletonEager
{
    // 1. 静态私有实例:类加载时直接初始化
    private static readonly SingletonEager _instance = new SingletonEager();

    // 2. 私有构造函数:阻止外部实例化
    private SingletonEager() { }

    // 3. 静态公共访问点:返回唯一实例
    public static SingletonEager Instance => _instance;
}

优缺点

  • 优点:实现简单,线程安全(无需额外处理)。
  • 缺点:无论是否使用,实例都会在类加载时创建(若实例初始化耗时,会影响程序启动速度)。
2. 懒汉式(Lazy Initialization,非线程安全)

特点:延迟初始化(第一次使用时才创建实例),但多线程环境下可能创建多个实例(非线程安全)。

csharp 复制代码
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时,若_instancenull,多个线程会同时进入if语句,创建多个实例,违反单例原则。
适用场景:仅适用于单线程环境(如简单工具类)。

3. 线程安全的懒汉式(双重检查锁定,Double-Check Locking)

特点 :结合延迟初始化和线程安全,通过lock加锁和双重判断避免并发问题,是实际开发中最常用的实现之一。

csharp 复制代码
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#静态内部类的特性实现延迟初始化和线程安全(推荐使用,简洁且无锁)。

csharp 复制代码
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>类实现延迟初始化,自带线程安全机制,无需手动处理锁逻辑。

csharp 复制代码
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及以上框架(现代项目基本都满足)。

二、单例模式的注意事项

  1. 防止反射攻击

    私有构造函数可能被反射强行调用(如typeof(Singleton).GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance).Invoke(null)),导致创建多个实例。

    防护方式:在构造函数中检查实例是否已存在,若存在则抛异常:

    csharp 复制代码
    private SingletonLazy()
    {
        if (_lazyInstance.IsValueCreated)
        {
            throw new InvalidOperationException("单例实例已存在,禁止重复创建");
        }
    }
  2. 序列化问题

    若单例类需要序列化(实现ISerializable),反序列化时可能创建新实例。需重写GetObjectData和反序列化构造函数,确保返回原实例:

    csharp 复制代码
    public void GetObjectData(SerializationInfo info, StreamingContext context)
    {
        // 序列化时不存储数据,反序列化时返回单例
    }
    
    private SingletonLazy(SerializationInfo info, StreamingContext context)
    {
        // 反序列化时强制返回现有实例
        if (_lazyInstance.IsValueCreated)
        {
            throw new InvalidOperationException("禁止反序列化创建单例");
        }
    }
  3. 单例的生命周期

    单例实例通常随应用程序生命周期存在(除非手动释放),适合管理全局资源。但过度使用会导致代码耦合度升高(依赖全局状态),不利于单元测试。

三、适用场景

  • 全局配置管理(如AppConfig):确保配置只加载一次,全局共享。
  • 日志记录器(如Logger):避免多个日志实例导致的文件锁冲突。
  • 数据库连接池:统一管理连接,防止连接数爆炸。
  • 缓存管理器:全局缓存实例,保证缓存数据一致性。

总结

单例模式的核心是"唯一实例 + 全局访问",在C#中推荐使用以下两种实现:

  • 简单场景(无参数、需简洁):静态内部类 或**Lazy<T>**。
  • 复杂场景(需线程安全、可能传递参数):双重检查锁定 或**Lazy<T>**(Lazy<T>更推荐,内置线程安全)。

使用时需注意线程安全、反射攻击和序列化问题,避免滥用单例导致代码灵活性下降。

相关推荐
YuanlongWang4 小时前
C#基础——GC(垃圾回收)的工作流程与优化策略
java·jvm·c#
Code_Geo4 小时前
agent设计模式:第二章节—路由
网络·设计模式·路由
太过平凡的小蚂蚁4 小时前
解耦的艺术:深入理解设计模式之命令模式
设计模式·命令模式
YuanlongWang5 小时前
C# 基础——多态的实现方式
java·c#
CodeCraft Studio5 小时前
PDF处理控件Aspose.PDF教程:在C#中将PDF转换为Base64
服务器·pdf·c#·.net·aspose·aspose.pdf·pdf转base64
Meteors.6 小时前
23种设计模式——外观模式(Facade Pattern)详解
设计模式·外观模式
胖虎16 小时前
iOS中的设计模式(九)- 外观模式 用外观模式点一份外卖:Swift 实战讲解
设计模式·外观模式
咕白m6258 小时前
C# 将多张图片转换到一个 PDF 文档
c#·.net
张人玉8 小时前
c#WPF基础知识
开发语言·c#·wpf