面向对象很好的解决了“抽象”的问题,但是必不可免地要付出一定的代价。
对于通常情况来讲,面向对象的成本大都可以忽略不计。但是某些情况,面向对象所带来的成本必须谨慎处理。
前面是利用抽象手段来实现松耦合的设计,但是抽象必不可免的会带来一定代价,比如虚函数(倍乘)
典型模式:
单件模式:Singleton
享元模式:Flyweight
只有这两个模式不是解决抽象问题,而是解决性能问题。
一:单例模式
(一)概念
保证了一个类只生成唯一的实例对象。保证一个类,只有一个实例存在,同时提供能对该实例加以访问的全局方法
(二)动机
在软件系统中,经常有这样一个特殊的类,必须保证它们在系统中只存在一个示例,才能确保他们的逻辑正确性、以及良好的效率。
如何绕过常规的构造器,提供一种机制来保证一个类只有一个实例?工厂模式绕过new是为了避开紧耦合,单例模式避开new,是解决性能问题
这个应该类设计者的责任,而不是使用者的责任。
解决方案: 1.将构造函数设置为私有的 2.提供一个全局的静态方法 3.定义一个静态指针,指向本类的变量的静态变量指针
(三)代码讲解(四种版本)
1.线程非安全版本(单线程OK)
class Singleton{ private: Singleton(); Singleton(const Singleton& other); public: static Singleton* getInstance(); static Singleton* m_instance; }; Singleton* Singleton::m_instance=nullptr; //线程非安全版本 Singleton* Singleton::getInstance() { //由于是在类的外部定义,也是静态方法,不用加static if (m_instance == nullptr) { m_instance = new Singleton(); } return m_instance; }
多线程模式,当多个线程都进入到if中,会导致创建多个实例对象
2.线程安全版本,但是锁的代价过高
//线程安全版本,但锁的代价过高 Singleton* Singleton::getInstance() { Lock lock; //锁的局部变量,会在函数结束时自动释放,我们也可以设置为全局变量锁。局部变量锁可用是因为这个函数是静态方法,所以大家获取的还是一个锁 if (m_instance == nullptr) { m_instance = new Singleton(); } return m_instance; }
问题:高并发时,代价过高,对读线程的锁来说是浪费的。读线程本来应该直接跳过创建实例,直接去使用唯一实例,但是这里还有等待锁的释放,是不必要的。
3.双检测锁(锁前锁后检查),但由于内存读写reorder不安全
//双检查锁,但由于内存读写reorder不安全 Singleton* Singleton::getInstance() { if(m_instance==nullptr){ Lock lock; if (m_instance == nullptr) { m_instance = new Singleton(); } } return m_instance; }
解决了上面的问题,但是还有新的问题出现:内存读写reorder不安全
reorder不安全::代码执行顺序不确定
在我们new一个对象时的假象顺序:
1 分配对象内存;2 调用构造器,执行初始化操作;3 将对象引用赋值给变量
CPU层面指令集(有可能reorder),就是说在实际运行过程中,以上指令可能发生重新排序。比如:以上代码2、3可能发生重新排序,但是1的顺序并不会被改变(因为2、3指令需要依托于1指令执行的结果)
1 分配对象内存;2 将对象引用赋值给变量;3 调用构造器,执行初始化操作
(执行第2步之后,另外一个线程进来发现m_instance不是null,但未执行构造器,对象状态不正确,而之前获取m_instance的线程已经开始使用了对象,会出错)
上述的双检查锁的代码,整体代码逻辑是没问题的,虽然是线程非安全的,但这不是程序员能够解决的了。究其原因,此处线程非安全是因为reorder机制。
所以,我们程序员需要借助编译器的新特性才能解决该问题。
4.C++11版本之后的跨平台实现(volatile)
//C++ 11版本之后的跨平台实现 (volatile) std::atomic<Singleton*> Singleton::m_instance; //原子对象 std::mutex Singleton::m_mutex; Singleton* Singleton::getInstance() { Singleton* tmp = m_instance.load(std::memory_order_relaxed); //屏蔽编译器reorder std::atomic_thread_fence(std::memory_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; }
四种版本场景使用:
版本一:单线程可用,足够好
版本二:多线程可用,不算错,代价过高
版本三:不能用,所有编译器例出问题概率较高
版本四:C++11前各个平台不同实现
(四)模式定义
保证一个类仅有一个实例,并提供一个该实例的全局访问点。
——《设计模式》GoF
(五)类图(结构)
(六)要点总结
1.Singleton模式中的实例构造器可以设置为protected以允许子类派生。
2.Singleton模式一般不要支持拷贝构造函数和Clone接口,因为这有可能会导致多个对象实例,与Singleton模式的初衷相违背。
3.如何实现多线程环境下安全的Singleton?注意对双检查锁的正确实现。
(七)案例实现
1.单线程版本
#include <iostream> #include <stdlib.h> using namespace std; class Singleton { private: static Singleton* m_singer; static int count; private: Singleton() { m_singer = NULL; count = 0; cout << "contruct function exec" << endl; } public: static Singleton* getInstance() { if (m_singer==NULL) { m_singer = new Singleton(); count++; } return m_singer; } static void printInfo() { cout << "create object " << count << endl; } }; Singleton* Singleton::m_singer = NULL; //懒汉模式,并没有创建对象 int Singleton::count = 0; void main() { cout << "Lazy pattarn:" << endl; Singleton* p1 = Singleton::getInstance(); Singleton* p2 = Singleton::getInstance(); if (p1!=p2) { cout << "not the same object" << endl; } else { cout << "the same object" << endl; } p1->printInfo(); p2->printInfo(); system("pause"); return; }