单例模式

1. 单例的定义❓

一个类只允许常见一个对象(或者实例),那这个类就是一个单例类

2. 单例的用处❗

从业务概念上,有些数据在系统中值应该保存一份,就比较适合设计为单例类。比如 ,系统 的配置信息类 ,除此之外。我们还可以使用单例解决资源访问冲突的问题。

3. 单例的实现 ❗

  • 饿汉式
    在类加载的期间,就已经将instance 静态实例初始化好了,所以,instance实例的创建是线程安全的。不过,这样的实现方式不支持延迟加载实例
public sealed class EagerSingleton
{
    // 静态字段在类加载时初始化(线程安全)
    private static readonly EagerSingleton _instance = new EagerSingleton();

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

    // 提供全局访问点
    public static EagerSingleton Instance => _instance;
}
  • 懒汉式
    懒汉式相对于饿汉式的优势是支持延迟加载,这种实现方式会导致频繁枷锁,释放锁,以及并发度低等问题,频繁的调用会产生性能瓶颈。
public sealed class EagerSingleton
{
    // 静态字段在类加载时初始化(线程安全)
    private static readonly EagerSingleton _instance = new EagerSingleton();

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

    // 提供全局访问点
    public static EagerSingleton Instance => _instance;
}
  • 线程安全式
    缺点​​:每次访问 Instance 都会加锁,性能较差。
public sealed class LockedLazySingleton
{
    private static LockedLazySingleton _instance;
    private static readonly object _lock = new object();

    private LockedLazySingleton() { }

    public static LockedLazySingleton Instance
    {
        get
        {
            lock (_lock) // 加锁保证线程安全
            {
                if (_instance == null)
                {
                    _instance = new LockedLazySingleton();
                }
                return _instance;
            }
        }
    }
}
  • 双重加锁
    volatile 防止指令重排序(确保其他线程看到完全初始化的实例)。
    只有第一次初始化时加锁,后续访问无锁,性能最优。
public sealed class DoubleCheckedLockingSingleton
{
    private static volatile DoubleCheckedLockingSingleton _instance;
    private static readonly object _lock = new object();

    private DoubleCheckedLockingSingleton() { }

    public static DoubleCheckedLockingSingleton Instance
    {
        get
        {
            if (_instance == null) // 第一次检查(无锁)
            {
                lock (_lock)
                {
                    if (_instance == null) // 第二次检查(避免竞争)
                    {
                        _instance = new DoubleCheckedLockingSingleton();
                    }
                }
            }
            return _instance;
        }
    }
}

4. 单例存在哪些问题❓

  • 单例对OOP 特性支持不友好
  • 单例会隐藏类之间的依赖关系
  • 单例对代码的扩展性不友好
  • 单例对代码的可测试性不友好
  • 单例不支持有参数的构造函数

5. 单例有什么替代的解决方案❓

为了保证全局唯一 ,除了使用单例,我们还可以用静态方法来实现,不过,静态方法这种实现思路,并不能解决全部的问题。可以用工厂模式,IOC容器,来保证,由程序员自己来保证(自己在编写代码的时候不要创建两个类对象)

如果单例类并没有后续扩展的需求,并且不依赖外部系统,那设计成单例类就没有太大问题

6. 如果理解单例模式的唯一性❓

单例类中对象的唯一性的作用范围是“进程唯一”的,进程唯一指的是进程内唯一,进程间不唯一;“线程唯一”值得是线程内唯一,线程间可以不唯一,实际上,“进程唯一” 就以为折线程内,线程间都唯一,这也是“进程唯一”和“线程唯一”的区别之外。“集群唯一”指的是进程内唯一,进程间也唯一。

7. 如何实现线程唯一的单例。❓

可以通过一个HashMap 来存储对象,其中Key 是线程ID ,value 是对象。这样我们就可以做到,不同的线程对象不同的对象,同一个线程只能对应一个对象。

8. 如何实现集群环境下的单例 ❓

我们需要把这个单例对象序列化并存储到外部共享存储区(比如文件)。进程在使用这个单例对象的时候,需要先从外部共享存储区中将它读取到内存,并反序列化成对象,然后再使用,使用完成之后还需要再存储回外部共享存储区。为了保证任何时刻在进程间都只有一份对象存在,一个进程在获取到对象之后,需要对对象加锁,避免其他进程再将其获取。在进程使用完这个对象之后,需要显式地将对象从内存中删除,并且释放对对象的加锁

9. 如何实现一个多例模式 ❓

“单例”指的是一个类只能创建一个对象。对应地,“多例”指的就是一个类可以创建多个对象,但是个数是有限制的,比如只能创建 3 个对象。多例的实现也比较简单,通过一个 Map 来存储对象类型和对象之间的对应关系,来控制对象的个数。