C++单例模式详解

  • 单例模式(Singleton):
    一种常用的设计模式,一般指在一个进程中使用单例模式的类只允许存在一个实例化对象,

  • 应用场景:

    1. 管理共享资源: 常见于程序配置文件,在整个程序运行期间,使用单例模式的类实例化对象来统一管理配置文件,该进程的其他对象通过访问该实例化对象来操作配置文件,在复杂的应用程序中简化了配置文件的管理。
    2. 功能单一但是不适合频繁实例化或者销毁的对象, 单例模式只允许创建一个对象,因此节省内存,加快对象访问速度,因此对象需要被公用的场合适合使用,如多个模块使用同一个数据源连接对象等等。如:
      1.频繁访问数据库或文件的对象。
      2.需要频繁实例化然后销毁的对象。
      3.创建对象时耗时过多或者耗资源过多,但又经常用到的对象。
      4.有状态的工具类对象。
  • 实现以及原则:
    实现单利模式的原则和过程:
    1.单例模式:确保一个类只有一个实例,自行实例化并向系统提供这个实例
    2.单例模式分类:
    饿单例模式(类加载时实例化一个对象给自己的引用),
    懒单例模式(调用取得实例的方法如getInstance时才会实例化对象)

下面是对应的C++单例模式对象推导过程,如果大家有兴趣可以慢慢看下去,想直接看结果的请看最后一个版本的代码。

  • 3.1版本代码

#pragma


#include <stdio.h>

class CSingletion
{
public:
	CSingletion* GetInstance()
	{	
		if (NULL == m_pStaticSingletion)
		{
			 m_pStaticSingletion = new CSingletion();
		}
		return m_pStaticSingletion;
	}
	//todo 实现功能
	void work();

private:

	CSingletion();
	CSingletion(const  CSingletion &);
	CSingletion& operator = (const CSingletion&);
	static CSingletion* m_pStaticSingletion;
};
CSingletion* CSingletion::m_pStaticSingletion = nullptr;//类静态成员需要外部初始化

缺点:没有线程安全,多线程并发同时调用GetInstance函数,会出现问题。
无法自动析构,会存在内存泄漏

  • 3.2版本代码
#pragma


#include <stdio.h>
#include <stdlib.h>

class CSingletion1
{
public:
	CSingletion1* GetInstance()
	{	
		if (NULL == m_pStaticSingletion)
		{
			m_pStaticSingletion = new CSingletion1();
			atexit(DeleteInstance);//登记析构函数,在进程退出时让进程来为我们调用析构
		}
		return m_pStaticSingletion;
	}
	//todo 实现功能
	void work();

private:

	static void DeleteInstance()
	{
		if (NULL != m_pStaticSingletion)
		{
			delete m_pStaticSingletion;
			m_pStaticSingletion = NULL;
		}
	}
	CSingletion1();
	CSingletion1(const  CSingletion1&);
	CSingletion1& operator = (const CSingletion1&);
	static CSingletion1* m_pStaticSingletion;
};
CSingletion1* CSingletion1::m_pStaticSingletion = nullptr;//类静态成员需要外部初始化

解决了自动析构的问题,

还可使用智能指针,内部类,局部静态实例化对象(而非指针,静态对象存储在全局数据段,系统会自动释放)来解决自动析构的问题。

  • 3.3版本代码
#pragma


#include <stdio.h>
#include <stdlib.h>
#include <mutex>

class CSingletion
{
public:
	CSingletion* GetInstance()
	{
		// 3.3.1
		std::lock_guard<std::mutex> lock(m_mutex);  C++11 作用域结束自动析构并解锁,无需手工解锁
		if (NULL == m_pStaticSingletion)
		{
			atexit(DeleteInstance);
			m_pStaticSingletion = new CSingletion();
			
		}
		return m_pStaticSingletion;
	}

	//todo 实现功能
	void work();

private:
	static void DeleteInstance()
	{
		if (NULL != m_pStaticSingletion)
		{
			delete m_pStaticSingletion;
			m_pStaticSingletion = NULL;
		}
	}

	CSingletion();
	CSingletion(const  CSingletion&);
	CSingletion& operator = (const CSingletion&);
	static CSingletion* m_pStaticSingletion;
	static std::mutex m_mutex;
};
CSingletion* CSingletion::m_pStaticSingletion = nullptr;//类静态成员需要外部初始化
std::mutex CSingletion::m_mutex;

添加一个锁那么就可以解决线程安全的问题,一般开发都会使用这种方式。

**

注意: 版本三对应的是单例模式的懒汉模式的线程安全加上内存自动管理的代码,一般看到这就可以正常使用单例模式了,下面基础比较薄弱的人可能有些难以理解。

**


  • 3.4版本代码
    仔细阅读版本三代码,我们会发现其实我们在外部判断指向实例化对象的静态指针是否为空时,其实相当于读操作,不需要加锁,因为加锁会导致系统调用,会减低系统的运行速度,当然,小规模并发不要紧,如果并发程度非常高的时候性能差距就体现出来了。
#pragma


#include <stdio.h>
#include <stdlib.h>
#include <mutex>

class CSingletion4
{
public:
	CSingletion4* GetInstance()
	{
		if (NULL == m_pStaticSingletion)
		{
			std::lock_guard<std::mutex> lock(m_mutex);
			if (NULL == m_pStaticSingletion)
			{
				atexit(DeleteInstance);
				m_pStaticSingletion = new CSingletion4();
			}
		}
		return m_pStaticSingletion;
	}
	//todo 实现功能
	void work();
private:
	static void DeleteInstance()
	{
		if (NULL != m_pStaticSingletion)
		{
			delete m_pStaticSingletion;
			m_pStaticSingletion = NULL;
		}
	}
	CSingletion4();
	CSingletion4(const  CSingletion4&);
	CSingletion4& operator = (const CSingletion4&);
	static CSingletion4* m_pStaticSingletion;
	static std::mutex m_mutex;
};
CSingletion4* CSingletion4::m_pStaticSingletion = nullptr;//类静态成员需要外部初始化
std::mutex CSingletion4::m_mutex;

所以我们可以将锁放在if(NULL == **)的语句的下面,如此,相比较版本三的代码,不管m_pStaticSingletion 是否为空,进来就加一次锁,版本四只会在m_pStaticSingletion 为空时加锁,会好很多, 那我们来讨论一种情况,当m_pStaticSingletion为空时,两个线程同时调用GetInstance函数,第一个线程的函数调用,已经进入到锁中了,

这里说明两个知识点

  1. m_pStaticSingletion = new CSingletion4();这条语句翻译为汇编指令或者说CPU指令主要分为三个过程,1.分配内存,2.调用构造函数, 3将内存首地址赋给m_pStaticSingletion 。
  2. cpu指令重排,简单地说,就是cpu中是通过流水线的方式来执行指令的,在不影响程序上下文的情况下, Cpu为了提高效率会对指令进行重排序,以适合cpu的顺序运行。但是指令重排会遵守As-if-serial的规则,就是所有的动作(Action)都可以为了优化而被重排序,但是必须保证它们重排序后的结果和程序代码本身的应有结果是一致的。所以这种情况在单线程中不会出现什么问题。而对于多线程,这个规则就失效了,所以可能会导致结果出现问题。这里不多赘述,推荐一个博客CPU指令重排,简单明了。

说白了还是优化出的问题,在java中有volatile可以禁止优化指令重排,但是C++volatile关键字不是这么用,言归正传,当第一个线程函数在执行到m_pStaticSingletion = new CSingletion4();这句话的时候,cpu进行了指令重排,就是cpu本来的执行顺序是1.分配内存,2.调用构造函数, 3将内存首地址赋给m_pStaticSingletion ,但是实际上步骤1执行完了,cpu中的一个流水线在执行步骤2,可能这个函数构造比较慢,当前cpu流水线发生了堵塞,那其他的cpu指令流水线不会等待,继续从指令缓冲区中取下一个指令步骤三,这个时候就是m_pStaticSingletion 指向的内存还未初始化,那么刚好,第二个线程执行到if (NULL == m_pStaticSingletion)语句的时候判断m_pStaticSingletion 不为空,直接将m_pStaticSingletion 返回出去,那么该线程在线程一构造函数为执行完成之前,调用该指针势必会导致程序异常,甚至崩溃。

综上所述,咱们的不可重入版本4的代码还是存在瑕疵, 所以引出了版本五,

  • 3.5版本代码
#pragma

#include <stdio.h>
#include <stdlib.h>
#include <mutex>
#include <atomic>

class CSingletion5
{
public:
	static CSingletion5* GetInstance()
	{
		CSingletion5* tmp = m_pStaticSingletion.load(std::memory_order_relaxed);
		std::atomic_thread_fence(std::memory_order_acquire);//获取内存屏障
		if (NULL == tmp)
		{
			std::lock_guard<std::mutex> lock(m_mutex);
			tmp = m_pStaticSingletion.load(std::memory_order_relaxed);
			if (NULL == tmp)
			{
				tmp = new CSingletion5();
				std::atomic_thread_fence(std::memory_order_release);//释放内存屏障
				m_pStaticSingletion.store(tmp, std::memory_order_relaxed);
				atexit(DeleteInstance);
			}
			
		}
		return m_pStaticSingletion; 
	}
	//todo 实现功能
	void work();

private:
	static void DeleteInstance()
	{
		if (NULL != m_pStaticSingletion)
		{
			delete m_pStaticSingletion;
			m_pStaticSingletion = NULL;
		}
	}

	CSingletion5();
	CSingletion5(const  CSingletion5&);
	CSingletion5& operator = (const CSingletion5&);
	static std::atomic<CSingletion5*> m_pStaticSingletion;
	static std::mutex m_mutex;
};
std::atomic<CSingletion5*> CSingletion5::m_pStaticSingletion = nullptr;//类静态成员需要外部初始化
std::mutex CSingletion5::m_mutex;

使用C++11提供的原子控制类atomic,加上内存屏障,防止cpu指令reorder,这个时候大家可能会觉得这个代码过于复杂,有没有更简单一点的方法呢,有的看代码六

  • 3.6版本代码
#pragma

//c++ 11 magic static 特性 当变量在初始化的时候,并且同时进入声明语句是,并发线程会阻塞等待初始化结束。
class CSingletion6
{
public:
	static CSingletion6& GetInstance()
	{
		//局部静态变量,在数据段,程序结束时系统会自动回收
		static CSingletion6 Singletion;
		return Singletion;

	}

	//todo 实现功能
	void work();

private:

	CSingletion6();
	CSingletion6(const  CSingletion6&);
	CSingletion6& operator = (const CSingletion6&);
	
};

这个代码看起来是非常的清爽了,非常的舒服,但是这个代码还是存在一些问题,因为构造函数是私有的·所以会造成无法继承以及拓展性太差的问题,为了解决这个问题我们提出了代码7,

  • 3.7版本代码
#pragma once
//c++ 11 magic static 特性 当变量在初始化的时候,并且同时进入声明语句是,并发线程会阻塞等待初始化结束。
template<typename T>
class CSingletion7
{
public:
	static T& GetInstance()
	{
		//局部静态变量,在数据段,程序结束时系统会自动回收
		static T Singletion;
		return Singletion;

	}

	virtual ~CSingletion7() {}

	//todo 实现功能
	void work();

protected:
	CSingletion7();
	CSingletion7(const  CSingletion7&);
	CSingletion7& operator = (const CSingletion7&);

};

class CSingletion8 : public CSingletion7<CSingletion8>
{
	friend class CSingletion7<CSingletion8>;//friends 让CSingletion7<CSingletion8>访问到CSingletion8的构造函数
	
//to do something else
	void Externwork();

private:
	CSingletion8();
	CSingletion8(const  CSingletion8&);
	CSingletion8& operator = (const CSingletion8&);

};

大家仔细看这个模板类的代码,我们将基类的构造函数可见性修改为protected,目的是为了让派生类可以访问到基类的构造函数,而将子类定义为派生类的友元类,是为了在基类中能够访问派生类的构造函数,这样我们在基类中定义了一个派生类对象,且该对象是唯一的,该对象既可以访问派生类的所有方法也可以访问子类的所有方法,至此大功告成。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值