5. 设计一个类,只能创建一个对象(单例模式)重点
5.1 设计模式
设计模式(Design Pattern)是一套被反复使用、多数人知晓的、经过分类的、代码设计经验的总结。为什么会有设计模式这种东西的出现呢?
最开始的代码设计是没有一定模式的,大家都是随便写的,写的多了就发现了一些套路,最终这些套路就被总结成了设计模式。
使用设计模式的目的:
- 为了代码可重用性、让代码更容易被他人理解、保证代码可靠性;
- 设计模式使代码编写真正工程化;
- 设计模式是软件工程的基石脉络,如同大厦的结构一样
5.2 单例模式
一个类只能创建一个对象,即单例模式,该模式可以保证系统中该类只有一个实例,并提供一个 访问它的全局访问点,该实例被所有程序模块共享。比如在某个服务器程序中,该服务器的配置 信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再 通过这个单例对象获取这些配置信息,这种方式简化了在复杂环境下的配置管理。
单例模式有两种实现方式:饿汉模式
he
5.2.1 饿汉模式
这里的饿汉是一个形象的说法,把程序比做一个饿汉,在main函数开始前就创建这个单例对象,就像一个饿汉一样。
饿汉模式的实现原理就是:把所有的构造都私有化或者delete掉,然后重新提供一个GetInstance方法用于获取这个单例
//单例模式 //懒汉模式 class Singleton { public: static Singleton& GetInstance()//获取这个单例对象 { return _ins; } //一些对应的数据操作 void Insert(pair<string, int> val) { _mp.insert(val); } void Print() { for(auto& kv : _mp) { cout << kv.first << ":" << kv.second << endl; } } private: Singleton() {}//把构造函数私有化 Singleton(const Singleton&) = delete;//删除拷贝构造和赋值重载 Singleton operator=(const Singleton&) = delete; private: static Singleton _ins;//一个静态的“全局变量”,用于在类外访问到构造函数的 //单例类的一些数据 map<string,int> _mp; }; Singleton Singleton::_ins;//在程序开始执行main函数之前就已经构造对象 int main() { auto& ins1 = Singleton::GetInstance();//调用的时候使用GetInstance给单例对象取别名 ins1.Insert({"sort", 1}); auto& ins2 = Singleton::GetInstance(); ins2.Insert({"string", 3}); auto& ins3 = Singleton::GetInstance(); ins3.Print(); return 0; }
饿汉模式下,单例对象在main函数被调用之前就已经构造,所以不存在线程安全的问题,但是同时也存在一些缺点
- 有的单例对象构造十分耗时或者需要占用很多资源,比如加载插件、 初始化网络连接、读取文件等等,会导致程序启动时加载速度慢。
- 饿汉模式在程序启动时就创建了单例对象,所以即使在程序运行期间并没有用到该对象,它也会一直存在于内存中,浪费了一定的系统资源。
- 当多个单例类存在初始化依赖关系时,饿汉模式无法控制。比如A、B两个单例类存在于不同的文件中,我们要求先初始化A,再初始化B,但是A、B谁先启动初始化是由OS自动进行调度控制的,我们无法进行控制。
5.2.2 懒汉模式
除了饿汉模式之外,还有一种单例模式的实现方法是懒汉模式,所谓懒汉模式就是在第一次使用到单例对象的时候再构造
class SlackerInstance { public: static SlackerInstance* GetInstance() { if(_pins == nullptr) { _pins = new SlackerInstance; } return _pins; } void Insert(pair<string, int> val) { _mp.insert(val); } void Print() { for(auto& kv : _mp) { cout << kv.first << ":" << kv.second << endl; } } private: SlackerInstance() {}//构造函数私有化 SlackerInstance(const SlackerInstance&) = delete;//删除拷贝构造和赋值重载 SlackerInstance operator=(const SlackerInstance&) = delete; private: static SlackerInstance* _pins;//静态的单例对象指针 map<string, int> _mp; }; SlackerInstance* SlackerInstance::_pins = nullptr;//初始化为空指针
找bug环节:请找出上述代码的bug
- 线程安全问题:GetInstance函数不是线程安全的,内部的new不是原子性操作;
问题的解决:在函数内部加锁
static SlackerInstance* GetInstance() { mtx.lock();//加锁 if(_pins == nullptr)//第一次获取单例对象的时候创建对象 { _pins = new SlackerInstance; } mtx.unlock();//完成new操作之后解锁 return _pins; }
但是,此时的代码还不够完美,每次调用GetInstance函数的时候都会进行无意义的加锁解锁操作,所以这里可以使用一种双检查的方法,在锁外层再进行一次判断
static SlackerInstance* GetInstance() { if(_pins == nullptr)//双检查,避免无意义的加锁解锁 { mtx.lock();//加锁 if(_pins == nullptr)//第一次获取单例对象的时候创建对象 { _pins = new SlackerInstance; } mtx.unlock();//完成new操作之后解锁 } return _pins; }
但是加锁之后就会出现另一个问题:new的过程中可能抛异常,此时就没有解锁,所以这里需要捕获异常进行解锁,然后重新抛出
static SlackerInstance* GetInstance() { if(_pins == nullptr)//双检查,避免无意义的加锁解锁 { mtx.lock();//加锁 try { if(_pins == nullptr)//第一次获取单例对象的时候创建对象 { _pins = new SlackerInstance; } } catch(...)//捕获异常并重新抛出 { mtx.unlock(); throw; } mtx.unlock();//完成new操作之后解锁 } return _pins; }
但是这样写看起来很low,还是追求高级的,优雅的写法
这里我们使用RAII的思想实现对锁的自动管理
//RAII锁的类 template<class Mutex> class LockGuard { public: LockGuard(Mutex& mtx) :_mtx(mtx) { _mtx.lock(); } ~LockGuard() { _mtx.unlock(); } private: Mutex& _mtx;//这里需要将锁设为引用的,因为锁不允许拷贝 };
当然,在库里面也实现了相关的类
static SlackerInstance* GetInstance() { if(_pins == nullptr)//双检查,避免无意义的加锁解锁 { //_mtx.lock();//加锁 LockGuard<std::mutex> lg(_mtx); if(_pins == nullptr)//第一次获取单例对象的时候创建对象 { _pins = new SlackerInstance; } //_mtx.unlock();//完成new操作之后解锁 } return _pins; }
单例对象的资源释放与数据保存
问题一:单例对象是new出来的需要进行释放吗?
由于单例对象的创建是全局的,全局资源在程序结束后会被自动回收 (进程退出后OS会解除进程地址空间与物理内存的映射)。但是我们也可以手动对其进行回收。需要注意的是,有时我们需要在回收资源之前将资源的相关数据保存到文件中,这种情况下我们就必须手动回收了。
我们可以在类中定义一个静态的 DelInstance 接口来回收与保存资源 (此函数不会被频繁调用,因此不需要使用双检查加锁)。
static void DelInstance() { //保存数据文件 //TODO //回收单例对象资源 std::lock_guard<std::mutex> lg(_mtx); if(_pins != nullptr) { delete _pins; _pins = nullptr; } }
当然,也可以定义一个内部的GC类,其在程序结束时自动调用析构函数
所以最终的实现方式如下
class SlackerInstance { public: static SlackerInstance* GetInstance() { if(_pins == nullptr)//双检查,避免无意义的加锁解锁 { //_mtx.lock();//加锁 LockGuard<std::mutex> lg(_mtx); if(_pins == nullptr)//第一次获取单例对象的时候创建对象 { _pins = new SlackerInstance; } //_mtx.unlock();//完成new操作之后解锁 } return _pins; } static void DelInstance() { //保存数据文件 //TODO //回收单例对象资源 std::lock_guard<std::mutex> lg(_mtx); if(_pins != nullptr) { delete _pins; _pins = nullptr; } } void Insert(pair<string, int> val) { _mp.insert(val); } void Print() { for(auto& kv : _mp) { cout << kv.first << ":" << kv.second << endl; } } class GC { public: ~GC() { if(_pins) { cout << "~GC()" << endl; DelInstance(); } } }; private: SlackerInstance() {}//构造函数私有化 SlackerInstance(const SlackerInstance&) = delete;//删除拷贝构造和赋值重载 SlackerInstance operator=(const SlackerInstance&) = delete; private: static SlackerInstance* _pins;//静态的单例对象指针 static mutex _mtx;//互斥锁 static GC _gc;//自动回收 map<string, int> _mp; }; SlackerInstance* SlackerInstance::_pins = nullptr;//初始化为空指针 mutex SlackerInstance::_mtx; SlackerInstance::GC SlackerInstance::_gc;
另一种版本的懒汉模式的写法
class SlackerInstance2 { public: static SlackerInstance2& GetInstance() { static SlackerInstance2 ins;//使用static修饰吗,第一次调用的时候构建对象,再次调用就直接使用 return ins; } void Insert(pair<string, int> val) { _mp.insert(val); } void Print() { for(auto& kv : _mp) { cout << kv.first << ":" << kv.second << endl; } } private: SlackerInstance2(){} SlackerInstance2(const SlackerInstance2&) = delete; SlackerInstance2 operator=(const SlackerInstance2&) = delete; private: map<string, int> _mp; };