单例模式
1. 定义
- 保证一个类有且仅有一个实例,并提供一个该实例的全局的访问点
2. 结构
- Singleton : 单例类,该类能且仅能创建一个全局唯一的实例,故其构造函数必须私有(防止外界访问),同时提供一个公共的全局接口来访问该实例
3. 示例
单例模式在不同使用场景下,可有多个版本的代码实现,每个版本都有相应优缺点,具体如下所示:
-
相关知识点回顾:
-
程序内存分区及其内存销毁情况:
内存堆区 : 指针,相应的内存需要手动销毁
内存栈区 : 存放局部变量,接口退出时相应变量自动销毁
常数区 : 存放常量
静态区 : 存在全局变量,程序退出时相应变量自动销毁
二进制代码区 :存在二进制代码
-
new 操作的内部实现分为3个步骤:
1、分配内存
2、调用构造函数
3、赋值操作正常情况下按序执行1、2、3, 但多线程环境下CPU可能会对这3个步骤进行指令重排reorder,故可能会出现1、3、2的执行顺序
-
-
版本1
-
实现:
Singleton* Singleton::instance = NULL; //静态成员需要初始化 class Singleton { public: static Singleton* GetInstance() { if (instance == NULL) { instance = new Singleton(); } return instance; } private: Singleton() {} // 构造 static Singleton *instance; // 全局唯一的实例 };
-
总结:
- 未定义析构函数,存在内存泄漏的问题
- 存在多线程不安全的问题
-
-
版本2
-
实现:
Singleton* Singleton::instance = NULL; #define USE_INNER_CLASS 0 class Singleton { public: static Singleton* GetInstance() { if (instance == NULL) { instance = new Singleton(); //方案1:使用atexit函数在程序退出时销毁已创建的实例 #if USE_INNER_CLASS atexit(Destructor); #endif } return instance; } //方案2:使用内部类的方式在程序退出时销毁已创建的实例 #if !USE_INNER_CLASS class GC { ~GC() { delete instance; } }; #endif private: static void Destructor() { if (NULL != instance) { delete instance; instance = NULL; } } Singleton(); // 构造 static Singleton *instance; // 全局唯一的实例 #if !USE_INNER_CLASS static GC gc; //使用内部类,程序退出时系统会自动释放gc #endif };
-
总结:
- 在版本1的基础上,使用atexit( )函数或者内部类在程序退出时销毁已创建的实例,以解决内存泄漏的问题
- 仍存在多线程不安全的问题
-
-
版本3
-
实现:
#include <mutex> Singleton* Singleton::instance = NULL; std::mutex Singleton::_mutex; class Singleton { public: static Singleton* GetInstance() { /* 方案1:在此处加锁,但是会导致锁的力度过大, 因为此接口读操作频次大于写操作,过于频繁的锁操作 会导致系统资源消耗过大的问题,故不能此处加锁 */ // std::lock_guard<std::mutex> lock(_mutex); if (NULL == instance) { // 方案2:需要在此处加锁 std::lock_guard<std::mutex> lock(_mutex); if (NULL == instance) { instance = new Singleton(); atexit(Destructor); } } return instance; } private: static void Destructor() { if (NULL != instance) { delete instance; instance = NULL; } } Singleton() {} // 构造 static Singleton *instance; // 全局唯一的实例 static std::mutex _mutex; // 互斥锁 };
-
总结:
- 在版本2的基础上,在访问实例前增加了加锁和双检测操作能大概率能减少多线程不安全问题的出现,但仍然无法根除
-
-
版本4
-
实现:
//此版本只适用于 C++11 编译环境, 编译时需带上 -std=c++11 #include <mutex> #include <atomic> //原子操作 std::atomic<Singleton *> Singleton::instance; //原子变量 std::mutex Singleton::_mutex; class Singleton { public: static Singleton *GetInstance() { //原子操作,从内存中读取变量的值 Singleton *tmp = instance.load(std::memory_order_relaxed); //获取内存屏障 std::atomic_thread_fence(std::memory_order_acquire); if (NULL == tmp) { std::lock_guard<std::mutex> lock(_mutex); tmp = instance.load(std::memory_order_relaxed); if (NULL == tmp) { tmp = new Singleton(); //释放内存屏障 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 (NULL != tmp) { delete tmp; } } Singleton() {} static std::atomic<Singleton *> instance; //原子变量 static std::mutex mutex; };
-
总结:
- 在版本3的基础上,将加锁操作改成原子操作,通过获取和释放内存屏障根除CPU指令重排的问题,从而解决多线程不安全的问题
- 该版本代码实现较为复杂,且只能在C++11下运行
-
-
版本5
-
实现:
/* 此版本只适用于 C++11 编译环境, 编译时需带上 -std=c++11 C++11 magic staic修饰符自带线程安全特性:如果当静态变量在初始化的时候,并发同时进入声明语句,并发线程将阻塞等待初始化结束 */ class Singleton { public: ~Singleton() {} static Singleton& GetInstance() { static Singleton instance; //静态局部变量, 程序退出将自动释放 return instance; //C++11 static修饰符 自带线程安全特性 } private: Singleton() {} }; #endif
-
总结:
- 在版本4的基础上进行代码简化,既保留了版本4的优点,又使得代码显得简单明了
- 利用静态局部变量的特性,实现延迟加载,系统自动回收内存,自动调用析构函数
- 静态局部变量初始化时,没有new操作带来的CPU指令reorder问题,同时在C++11编译器下又具有线程安全特性
-
-
版本6
-
实现:
template<typename T> class Singleton { public: static T& GetInstance() { /* 此处需要初始化DesignPattern,需要调用 Designpattern的构造函数,同时会调用父类的构造函数 */ static T instance; return instance; } virtual ~Singleton() {} protected: Singleton() {} //protected修饰构造函数,这样使得子类可以访问 }; class DesignPattern : public Singleton<DesignPattern> { //friend 能让Singleton<T >访问到Designpattern的构造函数 friend class Singleton<DesignPattern>; private: DesignPattern() {} };
-
总结:
- 在版本5的基础上,增加了模板,使得程序更具有通用性
-
4. 总结
- 优点:
- 提供了对唯一实例的受控访问
- 系统中有且仅有一个实例,节省了系统资源,同时提高了系统性能
- 缺点:
- 单例模式没有抽象层,不方便扩展
- 若一个实例负责多个职责但又必须唯一实例,则违背了 “单一职责” 原则