设计模式:单例
为什么会有单例模式?
在程序中对于某个类只想有一个对象,并且要限制对象的创建时会用到单例模式。
单例模式实现了:程序全局都可以共享使用一个单例对象,有方便的接口可以获取这个单例对象,禁止创建这个单例类的对象,只能通过设计的接口来使用。
实现方式
做到一下几点就可以实现单例模式:
1. 私有化构造函数和析构函数。使得无法随意的创建对象。
2. 对象实例是一个指针,作为这个类的静态成员变量储存。
3. 提供一个共有接口可以获取这个类静态实例成员变量。
在第一次使用时初始化:
程序都是不断迭代的过程,我们先写个最基本的单例模式:
// 版本一:最基础版本 class Singleton { public: static Singleton* GetInstance() { if (m_pInstance == NULL) { m_pInstance = new Singleton(); } return m_pInstance; } private: Singleton() {} ~Singleton() {} Singleton(const Singleton& other) {} Singleton& operator=(const Singleton& other) {} static Singleton* m_pInstance; }; Singleton* Singleton::m_pInstance = nullptr;
上个版本的问题是会发生内存泄漏,由于没有释放在堆上申请的内存。
解决的方法有两种:1. 智能指针。 2. 内置删除类
本人更偏向于用智能指针来解决。
智能指针解决内存泄漏
class Singleton { public: static shared_ptr<Singleton> GetInstance() { if (m_pInstance == NULL) { m_pInstance = shared_ptr<Singleton>(new Singleton()); } return m_pInstance; },private: Singleton() {} Singleton(const Singleton& other); Singleton& operator=(const Singleton& other); static shared_ptr<Singleton> m_pInstance; }; shared_ptr<Singleton> Singleton::m_pInstance = nullptr;
本人在尝试这种方案时遇到了几个问题分享下:
1. 新建时智能指针时使用std::make_shared(Singleton)() 会编译报错,跟make_shared内部实现有关还未研究具体原因。
解决方式:
1)使用上述代码,用临时变量 shared_ptr(new Singleton) 来代替。
在安全性和效率上make_shared是好于shared_ptr的构造函数的。这种解决方式有一定风险。
2)
参考一下大佬的讨论:
https://stackoverflow.com/questions/8147027/how-do-i-call-stdmake-shared-on-a-class-with-only-protected-or-private-const?rq=1
2. 析构函数去掉或公有化,因为Singleton对象的析构交给了shared_ptr,必须要公有化析构函数。
线程安全性:
该考虑线程安全性了。
也是考虑最简单的方式,价格互斥锁。
static shared_ptr<Singleton> GetInstance() { Lock lock; if (m_pInstance == NULL) { m_pInstance = shared_ptr<Singleton>(new Singleton()); } return m_pInstance; }
因为只是需要防止第一次调用时由于多线程导致的冲突问题,所以这种简单粗暴的加锁方式会影响之后的调用。
双检测锁:
static shared_ptr<Singleton> GetInstance() { if (m_pInstance == NULL) { m_mtx.lock(); if (m_pInstance == NULL) { m_pInstance = shared_ptr<Singleton>(new Singleton()); } m_mtx.unlock(); } return m_pInstance; }
解释:
1. 如果多个线程同时调用GetInstance(),mutex互斥下只有一个线程进入new Singleton,确保只有一个实例被创建。当实例被创建后,之前被lock住的线程会再次进入,这时就需要判断是否已经创建过,第二个非空判断的用途。
2. 第一层判断是为了效率,当实例已经被创建后就不需要在进锁了。
但是。。。。。
这种实现在多核系统下依然不安全,原因是:
虽然new singleton是一个语句,但在底层操作系统运行时可能被分为几部分执行,例如,当m_pInstance指针已经被赋值了,但Singleton对象还没有完成构造,这是其他线程再次进入时就有可能使用一个还未完全构造的对象。
c++11 的一些特性带来了解决方法:
1. 利用std::atomic保证指针操作的原子性。
2. 先用零时变量储存new出来的单例对象,最后再存在原子指针变量中。
atomic<Singleton*> Singleton::m_pInstance = nullptr; mutex Singleton::m_mtx; static Singleton* GetInstance() { Singleton* tmp = m_pInstance.load(); if (tmp == NULL) { lock_guard<mutex> lock(m_mtx); tmp = m_pInstance.load(); if (tmp == NULL) { tmp = new Singleton(); m_pInstance.store(tmp); } } return tmp; }
C++11 静态初始化器
C++11标准:
如果控制(control)在变量初始化时并发进入声明,并发执行应等待初始化完成。
static Singleton* GetInstance() { static Singleton instance; return &instance; }
通过编译器来解决是最简单的一种方式。