设计模式——单例模式
设计模式:
设计模式代表了最佳实践,是软件开发过程中面临一般问题的解决方案。 设计模式是一套被反复使用、经过分类、代码设计总结的经验。
单例模式
单例模式也叫单件模式。Singleton是一个非常常用的设计模式,几乎所有稍微大一些的程序都会使用到它,所以构建一个线程安全并且 高效的Singleton很重要。
1. 单例类保证全局只有一个唯一实例对象。
2. 单例类提供获取这个唯一实例的接口。
由于要求只生成一个实例,因此我们必须把构造函数的访问权限标记为protected或private,限制只能在类内创建对象.
单例类要提供一个访问唯一实例的接口函数(全局访问点),就需要在类中定义一个static函数,返回在类内部唯一构造的实例。
(这样还可以确保直接用类名就能访问到该唯一实例,不必用到实例化出的对象名去调用)
两个概念:
懒汉模式 (lazy loading ):第一次调用GetInstance才创建实例对象,比较复杂
饿汉模式: 程序一运行,就创建实例对象、简洁高效 ,但有些场景下不适用
方法一:不考虑线程安全,只适用于单线程环境的单例类
定义一个静态的实例,在需要的时候创建该实例 (懒汉模式)
class Singleton { public: //获取唯一对象实例的接口函数 static Singleton* GetInstance() { if (_instance == NULL) { _instance = new Singleton(); } return _instance; } static void DelInstance() { if (_instance != NULL) { delete _instance; _instance = NULL; } } void Print() { cout << _data << endl; } protected: //构造函数标记为protected或private,限制只能在类内创建对象 Singleton() :_data(5) {} //防拷贝 Singleton(const Singleton&); Singleton operator=(const Singleton&); private: //指向实例的指针定义为静态私有,这样定义静态成员函数获取对象实例 static Singleton* _instance; // 单实例对象 int _data; //单实例对象中的数据 }; // 静态成员在类外初始化 Singleton* Singleton::_instance = NULL;
这种方法是最简单、最普遍的方法。只有在_instance为NULL的时候才会创建一个实例以避免重复创建。同时我们把构造函数定义为私有函数,这样就能确保只创建一个实例。
但是上述的代码在单线程的时候工作正常,在多线程的情况下就有问题了。
设想如果两个线程同时运行到判断_instance是否为NULL的 if 语句那里,并且_instance之前并未创建时,这两个线程各自就都会创建一实例,这是就无法满足单例模式的要求了。
方法二:能在多线程环境下工作,但是效率不高
为了保障在多线程环境下只得到一个实例,需要加一把互斥锁。把上述代码稍作修改,即:
ps: 下面部分的加锁使用了C++11库的互斥锁
class Singleton { public: //获取唯一对象实例的接口函数 static Singleton* GetInstance() { //lock(); //C++中没有直接的lock() //RAII //lock lk; _sMtx.lock(); //C++11 if (_instance == NULL) { _instance = new Singleton(); } //unlock(); _sMtx.unlock(); return _instance; } static void DelInstance() { if (_instance != NULL) { delete _instance; _instance = NULL; } } void Print() { cout << _data << endl; } protected: //构造函数标记为protected或private,限制只能在类内创建对象 Singleton() :_data(5) {} //防拷贝 Singleton(const Singleton&); Singleton operator=(const Singleton&); private: //指向实例的指针定义为静态私有,这样定义静态成员函数获取对象实例 static Singleton* _instance; // 单实例对象 int _data; // 单实例对象中的数据 static mutex _sMtx; // 互斥锁 }; // 静态成员在类外初始化 Singleton* Singleton::_instance = NULL; mutex Singleton::_sMtx;
设想有两个线程同时想创建一个实例,由于在一个时刻,只有一个线程能得到互斥锁,所以当第一个线程加上锁后,第二个线程就只能等待。当第一个线程发现实例还没有创建时,它就建立一个实例。接着第一个线程释放锁,此时第二个线程进入并上锁,这个时候由于实例已经被第一个线程创建出来了,第二个线程就不会重复创建实例了,这样就保证在多线程环境下只能得到一个实例。
但是,每次获取唯一实例,程序都会加锁,而加锁是一个非常耗时的操作,在没有必要的时候,我们要尽量避免,否则会影响性能。
方法三:使用双重检查,提高效率,避免高并发场景下每次获取实例对象都进行加锁,并使用内存栅栏防止重排
class Singleton { public: //获取唯一对象实例的接口函数 static Singleton* GetInstance() { // 使用双重检查,提高效率,避免高并发场景下每次获取实例对象都进行加锁 if (_instance == NULL) { std::lock_guard<std::mutex> lck(_sMtx); if (_instance == NULL) { // tmp = new Singleton()分为以下三个部分 // 1.分配空间2.调用构造函数3.赋值 // 编译器编译优化可能会把2和3进行指令重排,这样可能会导致高并发场景下,其他线程获取到未调用构造函数初始化的对象 // 以下加入内存栅栏进行处理,防止编译器重排栅栏后面的赋值到内存栅栏之前 Singleton* tmp = new Singleton(); MemoryBarrier(); //内存栅栏 _instance = tmp; } } return _instance; } static void DelInstance() { if (_instance != NULL) { delete _instance; _instance = NULL; } } void Print() { cout << _data << endl; } protected: //构造函数标记为protected或private,限制只能在类内创建对象 Singleton() :_data(5) {} //防拷贝 Singleton(const Singleton&); Singleton operator=(const Singleton&); private: //指向实例的指针定义为静态私有,这样定义静态成员函数获取对象实例 static Singleton* _instance; // 单实例对象 int _data; // 单实例对象中的数据 static mutex _sMtx; // 互斥锁 }; // 静态成员在类外初始化 Singleton* Singleton::_instance = NULL; mutex Singleton::_sMtx;
试想,当实例还未创建时,由于 Singleton == NULL ,所以很明显,两个线程都可以通过第一重的 if 判断 ,进入第一重 if 语句后,由于存在锁机制,所以会有一个线程进入 lock 语句并进入第二重 if 判断 ,而另外的一个线程则会在 lock 语句的外面等待。而当第一个线程执行完 new Singleton()语句退出锁定区域,第二个线程便可以进入 lock 语句块,此时,如果没有第二重Singleton == NULL的话,那么第二个线程还是可以调用 new Singleton()语句,第二个线程仍旧会创建一个 Singleton 实例,这样也还是违背了单例模式的初衷的,所以这里必须要使用双重检查锁定(第二层if 判断必须存在)。
多数现代计算机为了提高性能而采取乱序执行,这使得内存栅栏成为必须。barrier就象是代码中的一个栅栏,将代码逻辑分成两段,barrier之前的代码和barrier之后的代码在经过编译器编译后顺序不能乱掉。也就是说,barrier之后的代码对应的汇编,不能跑到barrier之前去,反之亦然。之所以这么做是因为在我们这个场景中,如果编译器为了榨取CPU的performace而对汇编指令进行重排,其它线程获取到未调用构造函数初始化的对象,很有可能导致出错。
只有第一次调用_instance为NULL,并且试图创建实例的时候才需要加锁,当_instance已经创建出来后,则没必要加锁。这样的修改比之前的时间效率要好很多。
但是这样的实现比较复杂,容易出错,我们还可以利用饿汉模式,创建相对简洁高效的单例模式。
方法四:饿汉模式--简洁、高效、不用加锁、但是在某些场景下会有缺陷
因为静态成员的初始化在程序开始时,也就是进入主函数之前,由主线程以单线程方式完成了初始化,所以静态初始化实例保证了线程安全性。在性能要求比较高时,就可以使用这种方式,从而避免频繁的加锁和解锁造成的资源浪费。
class Singleton { public: //获取唯一对象实例的接口函数 static Singleton* GetInstance() { assert(_instance); return _instance; } void Print() { cout << _data << endl; } protected: //构造函数标记为protected或private,限制只能在类内创建对象 Singleton() :_data(5) {} //防拷贝 Singleton(const Singleton&); Singleton operator=(const Singleton&); private: static Singleton* _instance; // 单实例对象 int _data; // 单实例对象中的数据 }; Singleton* Singleton::_instance = new Singleton;
代码实现非常简洁。创建的实例_instance并不是在第一次调用GetInstance接口函数时才创建,而是在初始化静态变量的时候就创建一个实例。如果按照该方法会过早的创建实例,从而降低内存的使用效率。
方法五:方法四还可以再简化点
class Singleton { public: //获取唯一对象实例的接口函数 static Singleton* GetInstance() { static Singleton instance; return &instance; } void Print() { cout << _data << endl; } protected: //构造函数标记为protected或private,限制只能在类内创建对象 Singleton() :_data(5) {} //防拷贝 Singleton(const Singleton&); Singleton operator=(const Singleton&); private: int _data; // 单实例对象中的数据 };
实例销毁
此处使用了一个内部GC类,而该类的作用就是用来释放资源
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //带RAII GC自动回收实例对象的方式 /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// class Singleton { public: // 获取唯一对象实例的接口函数 static Singleton* GetInstance() { assert(_instance); return _instance; } // 删除实例对象 static void DelInstance() { if (_instance) { delete _instance; _instance = NULL; } } void Print() { cout << _data << endl; } class GC { public: ~GC() { cout << "DelInstance()" << endl; DelInstance(); } }; private: Singleton() :_data(5) {} static Singleton*_instance; int _data; }; // 静态对象在main函数之前初始化,这时只有主线程运行,所以是线程安全的。 Singleton* Singleton::_instance = new Singleton; // 使用RAII,定义全局的GC对象释放对象实例 Singleton::GC gc;
在程序运行结束时,系统会调用Singleton中GC的析构函数,该析构函数会进行资源的释放。