前言
代码在设计模式代码
单例模式
版本一:
class Singleton { public: static Singleton * GetInstance() { if (_instance == nullptr) { _instance = new Singleton(); } return _instance; } private: Singleton(){}; //构造 ~Singleton(){}; Singleton(const Singleton &) = delete; //拷⻉构造 Singleton& operator=(const Singleton&) = delete;//拷贝赋值构造 Singleton(Singleton &&) = delete;//移动构造 Singleton& operator=(Singleton &&) = delete;//移动拷贝构造 static Singleton * _instance; };
存在问题:
析构函数不会被调用,当我们使用这个单例来操作一个文件的时候,正常来说我们会在析构函数中调用close文件的操作(这是为了确保文件中的内容能刷新到文件中),但是因为这个版本中没有delete相关的操作,所以析构函数不会被调用到,这就导致程序退出的时候不会close文件的fd,最终导致文件缓冲区的内容不会刷新到文件中。这里引出版本2。
版本二:
class Singleton { public: static Singleton * GetInstance() { if (_instance == nullptr) { _instance = new Singleton(); atexit(Destructor); } return _instance; } private: static void Destructor() { if (nullptr != _instance) { // delete _instance; _instance = nullptr; } } Singleton(){}; //构造 ~Singleton(){}; Singleton(const Singleton &) = delete; //拷⻉构造 Singleton& operator=(const Singleton&) = delete;//拷贝赋值构造 Singleton(Singleton &&) = delete;//移动构造 Singleton& operator=(Singleton &&) = delete;//移动拷贝构造 static Singleton * _instance; }; Singleton* Singleton::_instance = nullptr;//静态成员需要初始化
存在问题:
这个版本解决了内存泄漏问题,atexit是一个线程安全的函数,但是这个版本仍然存在一些问题,当多线程访问的时候,很可能会创建多个对象出来,这里引出版本3。
版本三:
#include <mutex> class Singleton { // 懒汉模式 lazy load public: static Singleton * GetInstance() { // std::lock_guard<std::mutex> lock(_mutex); // 3.1 切换线程 if (_instance == nullptr) { std::lock_guard<std::mutex> lock(_mutex); // 3.2 if (_instance == nullptr) { _instance = new Singleton(); // 1. 分配内存 // 2. 调用构造函数 // 3. 返回指针 // 多线程环境下 cpu reorder操作 atexit(Destructor); } } return _instance; } private: static void Destructor() { if (nullptr != _instance) { delete _instance; _instance = nullptr; } } Singleton(){}; //构造 ~Singleton(){}; Singleton(const Singleton &) = delete; //拷⻉构造 Singleton& operator=(const Singleton&) = delete;//拷贝赋值构造 Singleton(Singleton &&) = delete;//移动构造 Singleton& operator=(Singleton &&) = delete;//移动拷贝构造 static Singleton * _instance; static std::mutex _mutex; }; Singleton* Singleton::_instance = nullptr;//静态成员需要初始化 std::mutex Singleton::_mutex; //互斥锁初始化
说明:
读操作是不需要加锁的,只要用锁锁住写操作就行,所以这里将锁从3.1处换到了3.2处。
存在问题:
虽然加了一把锁,但是没有考虑到cpu指令重排导致的执行序问题(C++11之后进入多核时代,出现了一些优化,比如说编译器重排、cpu重排等,从而引起顺序一致性问题),比如说针对new这个操作,可能在底层涉及到3条汇编语句(1.分配内存 2.调用构造函数 3.返回指针),但是因为C++11之后多核时代存在的执行序优化问题,可能导致底层这3条语句的顺序发生变化,比如说一个线程先执行了1,然后执行3,最后执行2(因为这一步比较耗时),但此线程在执行2的时候另外一个线程发现指针不为空直接返回这个实例并操作了这个实例中的一些指针成员,因为这个实例的指针成员还没在构造函数中初始化,最终可能出现异常而造成崩溃。这里之后引出版本4。
版本四:
此版本可以有两种写法,如果不考虑构造函数中使用_instance原子变量的话,这种写法和下种写法是一摸一样的;否则下种写法种用的memory_order_relaxed效率更高一点
static Singleton * GetInstance() { // 这种方式的原子变量同时解决了原子性、可见性、执行序三个问题 Singleton* tmp = _instance.load(std::memory_order_acquire); if (tmp == nullptr) { std::lock_guard<std::mutex> lock(_mutex); tmp = _instance.load(std::memory_order_acquire); if (tmp == nullptr) { tmp = new Singleton; _instance.store(tmp,std::memory_order_release); atexit(Destructor); } } return tmp; }
说明:
tmp = _instance.load(std::memory_order_acquire);
if (tmp == nullptr) {
tmp = new Singleton;
_instance.store(tmp,std::memory_order_release);
这里面load和store之间的执行序可以随便优化,但不能优化到memory_order_acquire上面的tmp,即这里面在优化的过程中另外一个线程的tmp不能直接返回出去,另外一个线程返回的是tmp = new Singleton中的三条汇编语句执行完之后的tmp,从而解决了上一个版本的问题。
// volitile #include <mutex> #include <atomic> class Singleton { public: static Singleton * GetInstance() { // 这条语句保证了原子性(即不会被拆分)和可见性问题(也就是可以看到最新的数据), // memory_order_relaxed是松散的意思,表示我可以随便优化,这条语句不能 // 解决执行序问题 Singleton* tmp = _instance.load(std::memory_order_relaxed); // 这里用内存屏障来解决执行序问题,目的是:假如说构造函数中使用了_instance原子变量, // 因为给_instance设置了memory_order_relaxed,这种松散的结构是一种速度最快的方式 std::atomic_thread_fence(std::memory_order_acquire);//获取内存屏障 if (tmp == nullptr) { std::lock_guard<std::mutex> lock(_mutex); tmp = _instance.load(std::memory_order_relaxed); if (tmp == nullptr) { tmp = new Singleton;// 这里只需要保证它是原子执行的,不会被其它线程打断,也即不论cpu怎么 // 优化执行的顺序,只要在其它线程 return tmp;的时候,本线程的new操作的3个汇编指令能够执行 // 完即可,从而不会出现版本三出现的问题。 std::atomic_thread_fence(std::memory_order_release);//释放内存屏障 _instance.store(tmp,std::memory_order_relaxed); atexit(Destructor); } } return tmp; } private: static void Destructor() { Singleton* tmp = _instance.load(std::memory_order_relaxed); if (nullptr != tmp) { delete tmp; } } Singleton(){}; //构造 ~Singleton(){}; Singleton(const Singleton &) = delete; //拷⻉构造 Singleton& operator=(const Singleton&) = delete;//拷贝赋值构造 Singleton(Singleton &&) = delete;//移动构造 Singleton& operator=(Singleton &&) = delete;//移动拷贝构造 static std::atomic<Singleton*> _instance; static std::mutex _mutex; }; std::atomic<Singleton*> Singleton::_instance;//静态成员需要初始化 std::mutex Singleton::_mutex; //互斥锁初始化 // g++ Singleton.cpp -o singleton -std=c++11
说明:
多核时代,编译器和CPU对我们的程序都会进行优化,(只要不影响后面结果的情况下)让我们的程序以最快的速度执行,但是这种优化也会带来问题,比如说会违反顺序一致性。
原子变量解决了三个问题:
- 原子执行的问题
- 可见性问题(用load和store来解决)
- 执行序问题
原子变量语义解释:
- load:指的是能够加载其它线程最新的可见的数据,我们可以可见其它线程操作的数据
- store:通常是写操作,我这个线程操作或者说修改的数据能够让其它线程可见
cpu指令重排相关问题解决方案:
- 可见性问题:通过load和store去解决
- 执行序问题:对应的是内存模型,C++一共提供了6种内存序,这里暂时只解释两种
memory_order_acquire:通常对应着读操作,意思是里面的语句不能优化到memory_order_acquire这个语句的上面去;
memory_order_release:指memory_order_release上面的代码不能优化下来;
上面这两个内存模型一起使用表示两者中间的代码既不能优化到上面去,也不能优化到下面去。
内存屏障解决了两个问题:
- 可见性问题
- 执行序问题
版本五:
// c++11 magic static 特性:如果当变量在初始化的时候,并发同时进⼊声明语句,并发线程将会阻塞等待初始化结束。 class Singleton { public: static Singleton& GetInstance() { static Singleton instance; return instance; } private: Singleton(){}; //构造 ~Singleton(){}; Singleton(const Singleton &) = delete; //拷⻉构造 Singleton& operator=(const Singleton&) = delete;//拷贝赋值构造 Singleton(Singleton &&) = delete;//移动构造 Singleton& operator=(Singleton &&) = delete;//移动拷贝构造 }; // 继承 Singleton // g++ Singleton.cpp -o singleton -std=c++11 /*该版本具备 版本5 所有优点: 1. 利⽤静态局部变量特性,延迟加载; 2. 利⽤静态局部变量特性,系统⾃动回收内存,⾃动调⽤析构函 数;没有new,不需要写delete释放内存 3. 静态局部变量初始化时,没有 new 操作带来的cpu指令 reorder操作; 4. c++11 静态局部变量初始化时,具备线程安全; */
存在问题:
若程序中有很多处都需要使用单例模式,每个都写一遍这种的话,显得很不妥,之后版本六使用模板类解决
版本六:
template<typename T> class Singleton { public: static T& GetInstance() { static T instance; // 这⾥要初始化DesignPattern,需要调⽤DesignPattern 构造函数,同时会调⽤⽗类的构造函数。 return instance; } protected: virtual ~Singleton() {} Singleton() {} // protected修饰构造函数,才能让别⼈继承 private: Singleton(const Singleton &) = delete; //拷⻉构造 Singleton& operator=(const Singleton&) = delete;//拷贝赋值构造 Singleton(Singleton &&) = delete;//移动构造 Singleton& operator=(Singleton &&) = delete;//移动拷贝构造 }; class DesignPattern : public Singleton<DesignPattern> { friend class Singleton<DesignPattern>; //friend 能让Singleton<T> 访问到 DesignPattern构造函数 private: DesignPattern() {} ~DesignPattern() {} };
说明:
写友元的目的是为了父类在构造子类的时候可以调用子类私有的构造函数。此版本解决了变化点(通过使用模板和继承的方式)的问题,如果项目中有多个类都要使用单例模式,那么就使用这种方式。
工厂模式
背景:
实现一个导出数据的接口,让客户选择数据的导出方式;
要点:
解决创建过程比较复杂,希望对外隐藏这些细节的场景;
- 比如连接池、线程池
- 隐藏对象真实类型;
- 对象创建会有很多参数来决定如何创建;
- 创建对象有复杂的依赖关系;
本质:
- 延迟到子类来选择实现
举例:
通常用于对象创建的过程比较复杂,用户只需要关注它的职责(即导出),不需要关注它具体是怎么导出的,比如说针对导出类型为json的,可能对象创建的过程中需要引入json库,然后读取一堆配置信息,很复杂;比如线程池这种,对于用户来说,如果封装的好,可能只需要一个push接口就够了。
抽象工厂
说明:
github上的代码有些问题
责任链模式
背景:
请求流程,1 天内需要主程序批准,3 天内需要项目经理批准,3 天以上需要老板批准;
要点:
- 解耦请求方和处理方,请求方不知道请求是如何被处理,处理方的组成是由相互独立的子处理构成,子处理流程通过链表的方式连接,子处理请求可以按任意顺序组合;
- 责任链请求强调请求最终由一个子处理流程处理;通过了各个子处理条件判断;
- 责任链扩展就是功能链,功能链强调的是,一个请求依次经由功能链中的子处理流程处理;
- 将职责以及职责顺序运行进行抽象,那么职责变化可以任意扩展,同时职责顺序也可以任意扩展;
本质:
分离职责,动态组合。
装饰器模式
背景:
普通员工有销售奖金,累计奖金,部门经理除此之外还有团队奖金;后面可能会添加环比增长奖金,同时可能针对不同的职位产生不同的奖金组合;
要点:
- 通过采用组合而非继承的手法, 装饰器模式实现了在运行时动态扩展对象功能的能力,而且可以根据需要扩展多个功能。 避免了使用继承带来的“灵活性差”和“多子类衍生问题”。
- 不是解决“多子类衍生问题”问题,而是解决“父类在多个方向上的扩展功能”问题;
- 装饰器模式把一系列复杂的功能分散到每个装饰器当中,一般一个装饰器只实现一个功能,实现复用装饰器的功能;
什么时候使用?
不影响其他对象的情况下,以动态、透明的方式给对象添加职责;每个职责都是完全独立的功能,彼此之间没有依赖;
本质:
- 动态组合
组合模式
什么时候使用组合模式?
- 如果你想表示对象的部分-整体层次结构,可以选用组合模式,把整体和部分的操作统一起来,使得层次结构实现更简单,从外部来使用这个层次结构也容易;
- 如果你希望统一地使用组合结构中的所有对象,可以选用组合模式,这正是组合模式提供的主要功能;
怎么实现?
因为要实现组合对象和单个对象的使用统一,所以可以让组合对象和单个对象继承自同一个接口,因为单个对象主要用于实现具体职责,所以重新基类的职责接口就行;而组合对象的职责需要委托给它下面的单个对象去执行,可以查看上面的代码