一、设计模式
设计模式是前人大佬们经过分类的、代码设计经验的总结,是能被人们反复使用,多数人知晓的套路。就像古时候人们打仗打多了发现打仗起始也是有套路的,前人经过总结写出孙子兵法这种书一样。
设计模式总的算下来共有23中,单例模式就是其中一种,也是最为常用的一种。接下来详细介绍下单例模式。
二、单例模式
当我们想要让一个类只能有一个对象被实例化,这就是单例模式的应用场景了。
单例模式就是要求一个类只能实例化一个对象。并且任意地方去调用的话都只能调用这同一个对象。并且给用户提供对此实例的全局访问。
常见的单例模式实现有两种模式:饿汉模式和懒汉模式
饿汉模式:
饿汉模式顾名思义,就像一个饿汉一样不管什么时候都要吃,也就意味着不管什么时候身边都要有吃的。那么对于我们的类来说,饿汉模式就是不管你将来用或不用,在程序一启动,就创建一个唯一的实例对象。
优点:1、相对简单。
2、如果这个单例对象在多线程高并发环境下频繁使用,性能要求较高,那么使用饿汉模式来避免资源竞争,提高响应速度更好。
缺点: 1、如果配置信息过多,饿汉模式启动就会非常慢。(如果一个程序启动需要耗费一个小时以上你还想去用吗?)
2、如果有多个单例类,而这些类之间互相有依赖关系,那么启动时就无法保证顺序。 (比如你吃饭前必须喝啤酒开胃,而买啤酒也需要去超市采购,那么吃饭就依赖于喝啤酒,喝啤酒就依赖于去超市采购,这就是三个类之间有依赖关系,但是饿汉模式是无法保证这些类的顺序的)。
class Singleton
{
public:
static Singleton* GetInstance() //提供获取对象的接口
{
return &_sinst; //返回唯一的那个对象
}
private:
//构造函数私有
Singleton()
{};
//c++11防拷贝
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
static Singleton _sinst; //static保证全局只有唯一的对象
};
Singleton Singleton::_sinst; //在程序入口之前就完成单例对象的初始化
此处有没有注意到并没有写析构函数?
不写析构函数是因为饿汉模式的对象是一个全局对象,出了作用域会自动调用析构函数。
而懒汉模式的对象是 new 出来的,new 是主动调用,并不会自己调用析构函数,所以我们可以自己来实现。
懒汉模式:
知道了饿汉是必须一直有吃的,那么懒汉的理解也就容易多了,就是快饿死了才去做饭,就像拖延症一样,当我们要用实例对象时才去创建对象。跟写时拷贝技术(Copy-On-Write)是一个道理。
什么时候用懒汉比较好呢?如果单例对象构造十分耗时或者占用很多资源,什么加载插件,读取文件等等别的操作,而这些事情可能这个对象运行时并不会用到,但是程序一运行还得加载,就导致程序启动非常慢,这时候懒汉模式就派上用场了。
优点:
1、第一次使用实例对象时才创建对象。
2、进程启动没有负担。
3、多个单例启动顺序可以自由控制。
缺点: 复杂。
class Singleton
{
public:
static Singleton* GetInstance()
{
if(nullptr == m_pInstance)
{
m_pInstance = new Singleton();
}
return m_pInstance;
}
private:
//构造私有
Singleton(){};
//防拷贝
Singleton(const Singleton&);
Singleton& operator=(const Singleton&);
static Singleton* m_pInstance; //单例对象指针
};
Singleton* Singleton::m_pInstance = nullptr;
这就是一个懒汉模式,但是这段代码还存在一些问题! 就是个懒汉模式无法保证线程安全,还有内存泄漏的问题。
首先我们解决内存泄露的问题,在饿汉模式结束时提到,因为懒汉模式的对象是 new 出来的,所以程序结束后不会调用析构函数,我们可以通过实现一个内部类做垃圾回收,代码如下:
class Singleton
{
public:
static Singleton* GetInstance()
{
if(nullptr == m_pInstance)
{
m_pInstance = new Singleton();
}
return m_pInstance;
}
// 实现一个内嵌垃圾回收类
class CGarbo {
public:
~CGarbo(){
if (Singleton::m_pInstance)
delete Singleton::m_pInstance;
}
};
// 定义一个静态成员变量,程序结束时,系统会自动调用它的析构函数从而释放单例对象
static CGarbo Garbo;
private:
//构造私有
Singleton(){};
//防拷贝
Singleton(const Singleton&);
Singleton& operator=(const Singleton&);
static Singleton* m_pInstance; //单例对象指针
};
Singleton* Singleton::m_pInstance = nullptr;
Singleton::CGarbo Garbo;
这个内嵌垃圾回收类,当程序结束后,系统自动调用类的析构函数,而这个析构函数里的工作被我们命令去释放 new 出来的这个对象,所以起到了防止内存泄漏的作用。
那么再看看如何保证线程安全呢?这里我们就用到了double check,代码如下:
class Singleton
{
public:
static Singleton* GetInstance()
{
if (m_pInstance == nullptr) //第二次检查
{
m_mtx.lock();
if (m_pInstance == nullptr)//第一次检查(double check详细解释往下看!!!)
{
m_pInstance = new Singleton();
}
m_mtx.unlock();
}
return m_pInstance;
}
// 实现一个内嵌垃圾回收类
class CGarbo {
public:
~CGarbo(){
if (Singleton::m_pInstance)
delete Singleton::m_pInstance;
}
};
// 定义一个静态成员变量,程序结束时,系统会自动调用它的析构函数从而释放单例对象
static CGarbo Garbo;
private:
//构造私有
Singleton(){};
//防拷贝
Singleton(const Singleton&);
Singleton& operator=(const Singleton&);
static Singleton* m_pInstance; //单例对象指针
static mutex m_mtx; //互斥锁
};
Singleton* Singleton::m_pInstance = nullptr;
Singleton::CGarbo Garbo;
mutex Singleton::m_mtx;
这样也保证了线程安全的问题,这就是一个完整的懒汉模式,那么什么是double check呢?那么我们来说叨说叨:
double check 解释:
为什么要使用double check呢?我们看看下面这些情况就明白了
普通模式:
m_mtx.lock();
if (nullptr == m_pInstance)
{
//如果有两个线程,当第一个线程走到这,第二个线程会被锁在锁外面起到单例的作用,
//但是有多个线程时,当第一个线程已经new出对象时,
//那么后面的线程就没必要继续再排队等着检查了,会造成浪费
m_pInstance = new Singleton();
}
m_mtx.unlock();
如果这样,那么多线程时,当第一个进程进入lock后创建对象,别的线程会在lock处等待,
if (nullptr == m_pInstance)
{
m_mtx.lock();
//由于此处没有第二道检查,所以当第一个线程解锁后,
//第二个线程就直接new出对象,导致不是单例对象。
m_pInstance = new Singleton();
m_mtx.unlock();
}
所以有多个线程时,double check
//这层检查是在已经有对象创建出来之后,当别的线程走到这时进行检查,
//如果已经有对象,那么直接返回,就不用再去进行锁的步骤,提高了效率
if (nullptr == m_pInstance)
{
m_mtx.lock();
//这层检查是第一个线程要new之前,判断是否已经创建出对象。
if (nullptr == m_pInstance)
{
m_pInstance = new Singleton();
}
m_mtx.unlock();
}
总而言之,double check的作用就是减少加锁的次数,提高了效率。