c++单例模式

一、设计模式

  设计模式是前人大佬们经过分类的、代码设计经验的总结,是能被人们反复使用,多数人知晓的套路。就像古时候人们打仗打多了发现打仗起始也是有套路的,前人经过总结写出孙子兵法这种书一样。
  设计模式总的算下来共有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的作用就是减少加锁的次数,提高了效率。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值