Singleton模式

    科技2022-07-11  106

    文章目录

    1. 背景2. 定义3. 代码实现(1) Singleton类的特点(2) Singleton类的代码实现(3) getInstance()的代码实现方案1:单线程版本,多线程不安全方案2: 锁后判断,线程安全,缺陷是代价太高方案3: 锁前判断,达不到单例的目的方案4:double check, 锁前锁后都加判断,由于内存读写reorder导致不安全方案5:限制reorder操作的double check

    未经授权,禁止转载!创作不易,尊重原创!~~

    1. 背景

    在软件系统中,经常有这样一些特殊的类,必须保证它们在系统中只存在一个实例,才能确保它们的逻辑正确性,以及良好的效率。 如何绕过常规的构造器,提供一种机制来保证一个类只有一个实例? 这应该是类设计这的责任,而不是使用者的责任。

    2. 定义

    保证一个类仅有一个实例,并提供一个该实例的全局访问点。 《设计模式》 GoF

    3. 代码实现

    (1) Singleton类的特点

    构造函数被被声明成私有函数,防止用户直接调用构造函数实例化类。在用户没有自己写构造函数的情况下,编译器会默认为类生成两个公有的构造函数:无参数构造函数和拷贝构造函数。因此除了用户自己写的构造函数会被声明成私有的,这两个函数也要确保是私有的。类的实例化是通过调用静态成员函数getInstance()来实现的,获取类的对象也是通过这个对象来实现的。

    (2) Singleton类的代码实现

    class Singleton { private: Singleton(); Singletion(const Singleton& other); public: static Singleton* getInstance(); static Singleton* m_instance; };

    (3) getInstance()的代码实现

    方案1:单线程版本,多线程不安全
    Singleton* Singleton::get_instance() { if (m_instance == nullptr) { m_instance = new Singleton(); } return m_instance; }
    方案2: 锁后判断,线程安全,缺陷是代价太高

    代价太高的原因 在多线程下,写操作是需要线程保护的,读操作是不需要线程保护的。对应到这里就是只有在第一次实例化Singletonl类的时候(写操作)需要加锁保护,后续调用getInstance函数获取对象指针的时候(读操作)不需要加锁保护。而方案2的代码实现,无论读写操作都需要加锁。这显然限制了读操作的效率, 不适合高并发的项目

    Singleton* Singleton::get_instance() { Lock lock; if (m_instance == nullptr) { m_instance = new Singleton(); } return m_instance; }
    方案3: 锁前判断,达不到单例的目的

    **达不到单例的目的的原因 线程A在执行完if判断后时间片耗完,此时线程B执行if判断。此时线程A和B判断的结果都是true, 都会执行判断体里面的代码,此时Singleton对象会被多次实例化。

    Singleton Singleton::get_instance() { if (m_instance == nullptr) { Lock lock; m_instance = new Singleton(); } return m_instance; }
    方案4:double check, 锁前锁后都加判断,由于内存读写reorder导致不安全

    由于内存读写reorder导致不安全的解释

    m_instance = new Singleton();

    这行代码编译器执行的时候的正常情况下会拆分成三个步骤:

    step1:分配一片内存空间用于实例化Singleton对象, step2:通过构造函数实例化对象,即对这片内存空间进行读写, step3:完成实例化后,将这片内存的首地址赋给指针m_instace。

    由于编译器的reorder优化,可能这行代码上述步骤完成,他可能将上述的step2和step3反过来执行。 应用到方案4的实现上,如果线程A使用了reorder优化,线程A执行完step1和step3,时间片耗完。此时线程B执行getInstance函数,线程B在第一个判断时,发现m_instance不为nullptr, 继续往下执行,由于线程A并没有对这片内存进行初始化,因此线程B从线程中获取的内容都是随机的。 由于这是编译器优化上的bug,理论上来说在代码上是无法修复的。除非用户显示指定编译器不对上述代码进行优化,这就是方案5的实现

    方案5:限制reorder操作的double check

    c# , java会通过volatile关键字来告诉编译器不要进行reorder,后来微软的c++平台VC++也加入了这个关键字。但跨平台的实现还是通过以下代码。

    // c++1版本之后的跨平台实现(volatile) std::atomic<Singleton*> Singleton::m_instance; std::mutex Singleton::m_mutex; Singleton* Singleton::getInstance() { Singleton* tmp = m_instance.load(std::memory_order_relaxed); std::atomic_thread_fence(std::mempry_order_acquire); // 获取内存fence if (tmp == nullptr) { std::lock_guard<std::mutex> lock(m_mutex); tmp = m_instance.load(std::memory_order_relaxed); if (tmp == nullptr) { tmp = new Singleton; std::atomic_thread_fence(std::memory_order_release); // 释放内存fence m_instance.store(tmp.std::memory_order_relaxed); } } return tmp; }
    Processed: 0.019, SQL: 8