单例模式

简介

单例模式是一种常见的设计模式,用于确保类只有一个实例,并提供一个全局访问点。以下是一个简单的单例模式的示例

双重检查锁定

经典的双重检查锁定是一种常见的在多线程环境下延迟初始化对象的方式。下面是一个使用双重检查锁定的单例模式的示例代码:

using System;

public sealed class Singleton
{
    private static volatile Singleton instance; // 使用 volatile 关键字确保 instance 变量在多线程环境下的可见性
    private static object syncRoot = new Object(); // 用于加锁的对象

    private Singleton() { }

    public static Singleton Instance
    {
        get
        {
            if (instance == null)
            {
                lock (syncRoot)
                {
                    if (instance == null)
                    {
                        instance = new Singleton();
                    }
                }
            }
            return instance;
        }
    }

    // 其他成员方法
    public void SomeMethod()
    {
        Console.WriteLine("Singleton method called.");
    }
}

 

在这个示例中,Singleton 类的实例被声明为 private static volatile Singleton instance

volatile 关键字确保当 instance 被初始化为实例时,对所有线程可见。

syncRoot 对象被用作互斥锁,以确保只有一个线程可以访问临界区代码。

Instance 属性中,使用双重检查锁定确保在实例不存在时才创建实例。

这种方式既保证了延迟加载又确保了线程安全。

优点:

  1. 延迟加载(Lazy Initialization):双重检查锁定允许对象在第一次被使用时进行初始化,而不是在程序启动时就创建。这可以节省资源,因为对象在需要时才会被创建。

  2. 线程安全:通过双重检查,确保了在多线程环境下只有一个实例被创建。这是通过在第一次检查前后使用锁来实现的,因此在多线程情况下也能正常工作。

缺点:

  1. 性能开销:在每次访问实例时都需要进行双重检查和加锁操作,这会带来一定的性能开销。虽然通过双重检查可以减少锁的竞争,但在高并发环境下仍然可能会存在性能问题。

  2. 可能的重排序问题:一些编程语言和编译器可能会对代码进行指令重排序,这可能会导致双重检查锁定失效。为了解决这个问题,需要使用 volatile 关键字来确保指令不会被重排序,

  3. 但是在某些平台上 volatile 语义可能不够强大,无法完全解决问题。

静态初始化

静态初始化是一种在 C# 中实现单例模式的方式,它利用类的静态构造函数在类加载时就创建单例实例。以下是使用静态初始化实现单例模式的示例代码:

public sealed class Singleton
{
    // 在类加载时就创建实例,保证线程安全
    private static readonly Singleton instance = new Singleton();

    // 私有构造函数,防止外部实例化
    private Singleton() { }

    // 公有静态属性,提供全局访问点
    public static Singleton Instance => instance;

    // 其他成员方法
    public void SomeMethod()
    {
        Console.WriteLine("Singleton method called.");
    }
}

在这个示例中,Singleton 类的实例 instance 被声明为 private static readonly,这保证了在类加载时就会被创建,并且只会被创建一次。由于静态构造函数只会在类加载时被调用一次,因此这种方式可以保证单例的线程安全性。

由于 instancereadonly 的,它只能在静态构造函数中被赋值,且只能赋值一次,这就确保了单例实例的唯一性。

通过公有的静态属性 Instance 提供了对单例实例的全局访问点。其他代码可以通过 Singleton.Instance 来获取单例实例,并调用其方法。

这种方式简单直接,且保证了线程安全性,是一种常见的实现单例模式的方式。

优点:

  1. 线程安全:静态初始化保证了在类加载时就创建单例实例,并且由于静态构造函数只会在类加载时被调用一次,因此在多线程环境下也能保证单例实例的唯一性。

  2. 简单明了:相比于其他实现方式,静态初始化的实现方式非常简单,只需要在类中声明一个静态字段,并在静态构造函数中初始化实例即可。

  3. 延迟加载:虽然是在类加载时就创建了实例,但由于静态构造函数只有在实例被请求时才会被调用,因此也可以看作是一种延迟加载的方式。

缺点:

  1. 无法实现懒加载:虽然可以看作是一种延迟加载,但实际上单例实例是在类加载时就被创建了。因此,如果应用程序中并不总是需要这个单例实例,静态初始化就会带来不必要的资源消耗。

  2. 无法处理异常:如果在静态构造函数中出现异常,那么该异常将无法被捕获,也无法通过其他方式处理。这可能导致程序的不稳定性。

  3. 不支持延迟初始化选项:由于实例是在类加载时就被创建的,因此无法通过其他方式控制实例的初始化时间,也无法实现延迟初始化。

  4. 不支持依赖注入:静态初始化方式下,单例实例的创建是在类的静态构造函数中进行的,无法通过构造函数参数等方式实现依赖注入,因此不太适合需要依赖注入的场景

使用 Lazy<T> 类

使用 Lazy<T> 类可以实现延迟加载的单例模式,同时确保线程安全。这是一种简洁而且安全的方式,适用于大多数情况。以下是使用 Lazy<T> 类的范式示例:

using System;

public sealed class Singleton
{
    // 使用 Lazy<T> 类确保延迟加载和线程安全
    private static readonly Lazy<Singleton> lazyInstance = new Lazy<Singleton>(() => new Singleton());

    // 私有构造函数,防止外部实例化
    private Singleton() { }

    // 公有静态属性,提供全局访问点
    public static Singleton Instance => lazyInstance.Value;

    // 其他成员方法
    public void SomeMethod()
    {
        Console.WriteLine("Singleton method called.");
    }
}

在这个示例中,Singleton 类的实例 lazyInstance 被声明为 private static readonly Lazy<Singleton>,它使用了 Lazy<T> 类,并在初始化时传递了一个匿名方法,以确保在需要时才会创建实例。

由于 Lazy<T> 类在其内部使用了线程安全的延迟初始化技术,因此它保证了在多线程环境下只有一个实例被创建,并且只会在需要时才会被创建,从而实现了延迟加载的效果。

通过公有的静态属性 Instance 提供了对单例实例的全局访问点,其他代码可以通过 Singleton.Instance 来获取单例实例,并调用其方法。

这种方式简洁而且安全,是一种常见的实现单例模式的方式。

优点:

  1. 延迟加载:Lazy<T> 类能够延迟初始化单例实例,即在第一次访问时才会创建实例,从而节省了资源。

  2. 线程安全:Lazy<T> 类内部使用了线程安全的延迟初始化技术,确保在多线程环境下只有一个实例被创建。

  3. 简洁性:使用 Lazy<T> 类实现单例模式的代码相对较少,逻辑清晰,易于理解。

  4. 性能优化:Lazy<T> 类在实现上考虑了性能优化,避免了不必要的锁竞争,提高了并发访问性能。

缺点:

  1. 额外开销:虽然 Lazy<T> 类能够延迟初始化单例实例,但在实例第一次被访问时会增加一些额外的开销,包括委托的创建、初始化等。

  2.  不支持配置选项:Lazy<T> 类的初始化参数相对较少,无法支持更多的配置选项,比如依赖注入等。因此,如果单例类需要依赖注入,可能不太适合使用 Lazy<T> 类。

  3.  不够灵活:Lazy<T> 类适用于大多数情况,但在一些特定场景下可能不够灵活,比如需要自定义初始化行为、需要使用其他同步机制等。

  4.  可序列化问题:如果单例类需要支持序列化和反序列化,需要格外注意 Lazy<T> 类的序列化行为,因为它的实现是基于委托和延迟加载的。

 应用场景

  1. 资源共享:当程序中的多个部分需要共享一个资源时,可以使用单例模式确保只有一个实例被创建,并且提供一个全局的访问点。

  2. 日志类:在应用程序中,通常会有一个日志类负责记录日志信息。使用单例模式可以确保只有一个日志对象,所有的日志信息都被写入同一个日志文件中。

  3. 数据库连接池:在使用数据库连接池时,为了避免频繁地创建和销毁连接对象,可以使用单例模式确保连接池只有一个实例,并且提供一个统一的接口来获取数据库连接。

  4. 配置信息类:在应用程序中,通常会有一个配置信息类负责读取和管理配置信息。使用单例模式可以确保配置信息类只有一个实例,所有的配置信息都被统一管理。

  5. 线程池:在多线程编程中,为了提高性能和资源利用率,通常会使用线程池来管理线程。使用单例模式可以确保线程池只有一个实例,所有的线程都被统一管理。

posted @ 2024-02-26 15:37  咸鱼翻身?  阅读(30)  评论(0编辑  收藏  举报