设计模式学习总结:(10)单例模式探讨
单例模式(Singleton)很简单,从名字也很容易知道解决的是唯一对象创建问题,很多时候,如果因为一个对象只需要存在一份,正常对象创建方式有种杀鸡用牛刀的感觉。同时,也不能假设用户素质足够高,至少我们要保证从语法上,多个对象存在是不合理的,我们所要做的,就是约束使用者的行为。
意图:
保证一个类仅有一个实例,并提供一个全局访问点。
在c++中为了限定对象的创建,我们需要把构造函数设置为私有,保证无法从外界构造,同时需要一个静态变量指针来保存唯一对象,最后至少还需要一个函数来获得这个唯一对象。
class Singleton{ private: Singleton()=default; static Singleton* _instance; public: static Singleton* getInstance(); }; Singleton* Singleton::m_instance=nullptr; //c++11 nullptr Singleton* Singleton::getInstance() { if (_instance == nullptr) { //多线程触发点 _instance = new Singleton(); } return _instance; }
李建忠老师的设计模式这里让我大开眼界,送上笔记一枚:
-------------------------------------------------------------note------------------------
这种实现方式在单线程下,已经很好了,但是再多线程下,存在安全隐患。
在多线程情况下,如果多个线程同时执行到 if 判断那里,当第一个线程进入判断,然后第二个线程也进入判断,依次类推,可能有多个线程进入判断,这样就造成多个实例被new出来,而最终只有一个能被获得,剩下的将成为内存泄露的一分子。所以,我们很自然的想到用线程锁来解决,假设有这样一个线程锁,我们可以这样实现:
Singleton* Singleton::getInstance() { Lock lock; if (_instance == nullptr) { _instance = new Singleton(); }
lock.relase(); return _instance; }
但是又考虑到,if判断,只有有限的一次可能执行到,剩下大超级大的一部分是不可能进入判断的,也就是说,仅仅为了有限的o(1)次,我们就每一次创建一个锁,然后释放,如果次数足够多,并发量足够大,效率有很大的影响。很自然是不允许的,对于很多需要效率的场景,需要有更好的做法,所以有了曾经风靡一时的双重锁解法。
Singleton* Singleton::getInstance() { if(_instance==nullptr) { Lock lock; if (_instance == nullptr) { _instance = new Singleton(); } lock.realse(); } return _instance; }
保证了在足够多次的情况下,都不会获得锁,即使有幸获得锁,我们在进行判断,防止漏网之鱼,其实这代码从高级语言层面上看已经很完美了,然而,这里面竟然存在安全隐患。
叫做内存reorder隐患。 参考资料:链接
简单的说,_instance = new Singleton() 我们这一句的理想顺序是 创建空间->构造对象->赋值给指针。但是底层基于效率考虑,可能会编译成这样一种执行顺序,就是 创建空间->赋值给指针->构造对象。这样就造成如果刚好某个线程执行到了,赋值给指针,这一步,然后切换到另一个线程它判断,发现指针不是null,于是它就直接返回对象,注意,这个时候它返回了一个还未构造属性的对象。这就是问题所在。
最后,附上c++11新标准的解决方案,直接复制代码过来。
std::atomic<Singleton*> Singleton::_instance; std::mutex Singleton::m_mutex; Singleton* Singleton::getInstance() { Singleton* tmp = _instance.load(std::memory_order_relaxed); std::atomic_thread_fence(std::memory_order_acquire);//获取内存fence if (tmp == nullptr) { std::lock_guard<std::mutex> lock(m_mutex); tmp = _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; }
--------------------end----------------------------
下面写一些见解,纯思考:
----------------------------------------------------------语法思考-------------------------------------------------------------------------------------
c++类里面,static 可以定义成指针,也能定义成普通的成员。
抛出疑问:
class Singleton { Singleton()=default; static Singleton _instance; public: static Singleton getInstance(); }; Singleton Singleton::_instance = Singleton(); Singleton Singleton::getInstance() { return _instance; }
这种方式好像也能解决多线程问题,我不知道存不存在安全隐患,暂且保留,后续补充。
另外,我们对比一下一个类里面如果这三种定义:
class A { static A a; //合理 static A *b; //合理 A *c; //合理 A d;//不合理 }
很正常的语法,之前也做过思考,我们只要知道第四个为什么不合理,一切都明了了。在类实例被创建的时候,如果是第四种形式,你可以想象,这样的定义是需要默认初始化,或者如果你给它一个类。但是他本身是一个A实例,这个A实例也存在这样的一个A实例,于是A实例里面有个A1实例,A1实例里面有个A2实例,何时是终结。就好像山上有个庙,庙里有个和尚,和尚说:“山上有个庙。。。”。
如果是指针,那么可以默认初始化为null,这样null就不存在A实例,就不会进入和尚的庙。至于静态变量,如果你理解它的内存空间,其实不管是任何实例,都是同一个A实例,是唯一的同一个。基于此,我做过一个耐人寻味的测试:
class A { static A a; public: int b; } A A::a = A();
int main() { A test = A(); test.a.a.a.a.a.b = 1; cout << test.a.b<<endl; test.a.a.a.a.a.a.a.a.a.a.a.b = 2; cout << test.a.b<<endl; return 0; }
能猜到结果吗。
------------------------------------------------------------end--------------------------------------------------------------------------------------