设计模式-单例模式

1、定义与概念

单例模式是一种设计模式,它确保一个类只有一个实例,并提供一个全局访问点来访问这个实例。就像是在一个软件系统中,某些资源或者对象只需要存在一个就可以满足系统的需求,比如系统的配置管理器、数据库连接池等。通过单例模式,可以避免创建多个实例导致的资源浪费、数据不一致等问题。

2、实现方式

a、懒汉式单例(线程不安全)

  • 代码示例(C++)
cpp 复制代码
class Singleton {
private:
    static Singleton* instance;
    Singleton() {}
public:
    static Singleton* getInstance() {
        if (instance == nullptr) {
            instance = new Singleton();
        }
        return instance;
    }
};
Singleton* Singleton::instance = nullptr;
  • 分析

这种方式在首次调用 getInstance 方法时才创建实例。但是在多线程环境下是不安全的,因为多个线程可能同时判断 instancenullptr,然后都创建一个新的实例,这就违背了单例模式的初衷。

b、懒汉式单例(线程安全)

  • 代码示例(C++)
cpp 复制代码
class Singleton {
private:
    static Singleton* instance;
    static std::mutex mutex_;
    Singleton() {}
public:
    static Singleton* getInstance() {
        std::lock_guard<std::mutex> guard(mutex_);
        if (instance == nullptr) {
            instance = new Singleton();
        }
        return instance;
    }
};
Singleton* Singleton::instance = nullptr;
std::mutex Singleton::mutex_;
  • 分析

getInstance 方法中添加了互斥锁(mutex)来保证在多线程环境下只有一个线程能够创建实例。lock_guard 是一个 RAII(Resource Acquisition Is Initialization)机制的类,它在构造函数中自动锁定互斥锁,在析构函数中自动解锁,确保了锁的正确使用,避免了死锁等问题。

c、饿汉式单例模式

  • 代码示例(C++)
cpp 复制代码
class Singleton {
private:
    static Singleton* instance;
    Singleton() {}
public:
    static Singleton* getInstance() {
        return instance;
    }
};
Singleton* Singleton::instance = new Singleton();
  • 分析

这种方式在程序启动时就创建了单例实例。优点是线程安全 ,因为在程序运行时单例实例已经存在,不存在多个线程竞争创建实例的情况。但是可能会导致程序启动时间变长,因为在初始化阶段就创建了实例,即使这个实例可能在很长时间之后才会被用到。

3、应用场景

  • 配置管理类:在一个软件系统中,配置信息通常是全局唯一的。通过单例模式创建一个配置管理类,可以方便地在整个系统中访问和修改配置信息。例如,一个服务器应用程序可能有一个配置文件,用于存储服务器的端口号、数据库连接信息等,配置管理类可以以单例模式存在,确保所有模块都能访问到相同的配置信息。

  • 日志记录器:用于记录系统运行过程中的各种信息,如错误信息、调试信息等。整个系统通常只需要一个日志记录器,这样可以统一管理日志的输出格式、输出位置等。通过单例模式,可以确保在不同的模块中记录的日志都能够按照统一的规则进行处理。

  • 数据库连接池 :在需要频繁访问数据库的应用程序中,为了避免频繁地创建和销毁数据库连接,提高性能,可以使用数据库连接池。连接池可以以单例模式存在,它负责管理一定数量的数据库连接,当有模块需要访问数据库时,从连接池中获取连接,使用完毕后再归还连接,确保系统中只有一个连接池实例在管理数据库连接。

4、单例模式优缺点

  • 优点

    • **资源共享与节约:**单例模式确保在整个应用程序中只有一个实例存在,这对于一些资源密集型的对象(如数据库连接池、线程池)非常有用。以数据库连接池为例,创建和维护数据库连接是比较耗费资源的操作。单例模式下的数据库连接池可以在整个应用中被共享,避免了频繁创建和销毁连接,从而节省系统资源,提高性能。

    • **全局访问点方便使用:**单例模式提供了一个全局访问点,使得在应用程序的任何地方都能够方便地获取到该单例对象。例如,在一个复杂的软件系统中,配置管理类作为单例,可以很容易地被不同模块访问,从而获取系统的配置参数。这种统一的访问方式有助于代码的组织和维护,提高了代码的可读性和可维护性。

    • **控制共享资源访问:**对于一些需要在多个部分之间共享且需要严格控制访问的资源,单例模式能够提供有效的管理。例如,在一个日志记录系统中,单例的日志记录器可以确保所有的日志消息都按照统一的格式和存储方式进行处理,避免了多个日志记录实例可能导致的混乱,同时也方便对日志记录的级别、输出位置等进行集中控制。

  • 缺点

    • **违反单一职责原则:**单例类往往会承担过多的职责。因为它在整个系统中是唯一的实例,所以可能会被赋予过多的功能,这就导致了单例类可能会与多个模块产生复杂的交互,使得代码的耦合度增加。例如,一个单例的系统管理类可能既要负责系统的配置管理,又要负责系统的状态监控,这样当系统功能发生变化或者需要扩展时,单例类就会变得难以维护。

    • 对单元测试不友好:单例模式在单元测试中可能会带来一些困难。由于单例类的实例是全局唯一的,在测试过程中很难模拟不同的状态或者替换为测试替身(如模拟对象或桩对象)。例如,在测试一个依赖于单例数据库连接池的模块时,很难隔离数据库连接池对测试的影响,因为无法轻易地创建一个独立于真实数据库连接池的测试环境。

    • **可能导致隐藏的依赖关系:**在应用程序的多个部分都使用单例对象时,会产生隐藏的依赖关系。这种依赖关系在代码结构中可能不明显,当需要对单例类进行修改或者替换时,可能会影响到许多依赖它的模块,从而增加了系统的维护成本和风险。例如,一个原本用于本地存储的单例配置管理器,在系统需要支持云端配置存储时,由于很多模块都依赖这个单例配置管理器,修改起来就会比较复杂。

5、多线程环境下如何保证单例模式的线程安全?

a、双重检查锁定(Double - Checked Locking)机制

  • 原理

这种方法结合了懒汉式单例的延迟初始化优势和高效的线程安全机制。在第一次检查 if (instance == nullptr) 时,没有加锁,这是为了避免在单例实例已经创建后的每次访问都要获取锁,从而提高性能。只有当实例尚未创建时,才会进入第二次检查,这次检查是在加锁的情况下进行的,确保在多线程环境下只有一个线程能够创建实例。

  • 代码示例(以 C++ 为例)
cpp 复制代码
class Singleton {
private:
    static Singleton* instance;
    static std::mutex mutex_;
    Singleton() {}
public:
    static Singleton* getInstance() {
        if (instance == nullptr) {
            std::lock_guard<std::mutex> guard(mutex_);
            if (instance == nullptr) {
                instance = new Singleton();
            }
        }
        return instance;
    }
};
Singleton* Singleton::instance = nullptr;
std::mutex Singleton::mutex_;
  • 注意事项

在 C++ 11 之前,由于编译器和处理器的优化可能会导致双重检查锁定出现问题。因为编译器可能会对指令进行重排序,导致在对象还没有完全初始化时,就将指针赋值给 instance,其他线程可能会访问到未完全初始化的对象。在 C++ 11 中,这种问题得到了一定程度的缓解,因为新的内存模型保证了在 new 操作时初始化和指针赋值的原子性。不过在一些复杂的编译器和硬件环境下,还是需要谨慎使用。

b、使用静态局部变量(C++ 11 及以上)

  • 原理

C++ 11 保证了在多线程环境下,静态局部变量的初始化是线程安全的。当第一次调用 getInstance 函数并访问静态局部变量 instance 时,编译器会自动保证只有一个线程能够进行初始化操作,其他线程会等待初始化完成后再访问。这种方式实现起来比较简单,同时也具有懒汉式单例的延迟初始化特点。

  • 代码示例(以 C++ 为例)
cpp 复制代码
//代码实例(线程不安全)
template<typename T>
class Singleton
{
public:
static T& getInstance() 
{
    static T instance;
    return instance;
}
    
private:
    Singleton(){};
    Singleton(const Singleton&);
    Singleton& operator=(const Singleton&);
};
  • 注意事项

这种方式的缺点是,如果单例类的构造函数比较复杂或者有依赖关系,可能会导致一些隐藏的问题。例如,单例类的构造函数中有一些全局变量或者其他单例类的依赖,可能会出现初始化顺序的问题。

c、饿汉式单例模式(Eager Initialization)

  • 原理

在程序启动时就创建单例实例,这样在多线程环境下就不存在多个线程竞争创建实例的情况。因为单例实例在任何线程访问之前就已经存在,所以是线程安全的。

  • 代码示例(以 C++ 为例):
cpp 复制代码
class Singleton {
private:
    static Singleton* instance;
    Singleton() {}
public:
    static Singleton* getInstance() {
        return instance;
    }
};
Singleton* Singleton::instance = new Singleton();
  • 注意事项

饿汉式单例模式的主要缺点是可能会导致程序启动时间变长,因为在初始化阶段就创建了实例,即使这个实例可能在很长时间之后才会被用到。而且如果单例类的构造函数抛出异常,可能会导致程序启动失败,因为在程序启动时就会尝试创建单例实例。

相关推荐
晨米酱12 小时前
JavaScript 中"对象即函数"设计模式
前端·设计模式
数据智能老司机17 小时前
精通 Python 设计模式——分布式系统模式
python·设计模式·架构
数据智能老司机18 小时前
精通 Python 设计模式——并发与异步模式
python·设计模式·编程语言
数据智能老司机18 小时前
精通 Python 设计模式——测试模式
python·设计模式·架构
数据智能老司机18 小时前
精通 Python 设计模式——性能模式
python·设计模式·架构
使一颗心免于哀伤18 小时前
《设计模式之禅》笔记摘录 - 21.状态模式
笔记·设计模式
数据智能老司机2 天前
精通 Python 设计模式——创建型设计模式
python·设计模式·架构
数据智能老司机2 天前
精通 Python 设计模式——SOLID 原则
python·设计模式·架构
烛阴2 天前
【TS 设计模式完全指南】懒加载、缓存与权限控制:代理模式在 TypeScript 中的三大妙用
javascript·设计模式·typescript
李广坤2 天前
工厂模式
设计模式