设计模式:单例
为什么会有单例模式?
在程序中对于某个类只想有一个对象,并且要限制对象的创建时会用到单例模式。
单例模式实现了:程序全局都可以共享使用一个单例对象,有方便的接口可以获取这个单例对象,禁止创建这个单例类的对象,只能通过设计的接口来使用。
实现方式
做到一下几点就可以实现单例模式:
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) { // test1 m_mtx.lock(); if (m_pInstance == NULL) { // test2 m_pInstance = shared_ptr<Singleton>(new Singleton()); } m_mtx.unlock(); } return m_pInstance; }
解释:
1. 如果多个线程同时调用GetInstance(),mutex互斥保证只有一个线程进入lock后的代码,其他的线程在lock前等待。
当第一个线程创建了实例后释放锁,其他线程再次进入lock之后的代码,这时就不需要再次创建了,所以在test2这里要加上一个判断。
2. 对于已经创建好的实例,再次进入GetInstance时只需要返回指针即可,这就是test1进行判断的原因。
运行时序(memory-order):
上述双检测锁还是不够完善,因为:
在多核操作系统下同一句命令可能在底层分为多个命令执行,并且顺序不确定,当多个线程同时访问时可能出现不可预知的结果。
针对创建Singleton的过程,有可能m_pInstance指针已经被赋值了,但Singleton构造还未完成,此时另一个线程进入GetInstance的函数,判断指针不为空就返回使用还未构造完成的Singleton对象。
可以使用原子操作来避免这种情况。
std::atomic<Singleton *> Singleton::m_instance; std::mutex Singleton::m_mutex; Singleton *Singleton::getInstance() { Singleton *tmp = m_instance.load(); // 1 if (tmp == nullptr) { std::lock_guard<std::mutex> lock(m_mutex); tmp = m_instance.load(); if (tmp == nullptr) { tmp = new Singleton; m_instance.store(tmp); // 2 } } return tmp; }
静态初始化器(C++11)
其实还有一种最为简便的实现方式,需要依赖C++11之后的语言特性。
Singleton &Singleton::getInstance() { static Singleton instance; return instance; }
原文:https://www.cnblogs.com/dylan-liang/p/14720296.html