c++ 单例模式与多线程

单例模式

  单例模式的概念比较简单,即某一类只能创建一个对象,这样设计类的方式就是单例模式。
  在直接看代码之前,可以稍微思考一下这个单例模式大概会是个什么情况。
  
  在C++中,调用一次构造函数就会创建一个对象,这个机制是货真价实的,因此也可以推测出,单例模式某种程度上实现了构造函数的“隐藏”。
  构造函数怎么藏这个具体想不到,不过大致能想出来一个点,应该将构造函数放在private区域内,令其属于类的私有成员,这样在对象中(类的区域之外)就不能直接调用构造函数了。如果不这样做,构造函数天天露在外面,实在藏不住。
  另外还可以考虑到一个点:类的静态成员。类的静态成员并不依附于类的对象实例存在,无论对象的情况如何,静态成员都是由所有对象共享的,是唯一的存在。既然如此,会不会使用静态成员对单例模式的实现进行一种“管控”呢?
  
下面是单例模式的实现代码,注意代码主要使用了C++中类的静态成员知识。
  头文件的内容:

class MyClass
{
private:
	MyClass() {};
private:
	static MyClass* m_instance;

public:
	static MyClass* GetInstance();
	void func(int a);
};


  先别急着看cpp文件中的实现,先来鉴赏一下头文件中的内容:
  首先,不出意料的是,构造函数成为了私有成员,也就是说,如果我们想要调用构造函数新建一个实例对象,我们至少还需要调用一个函数,在这个函数里调用构造函数。
  接下来出现了一个静态成员变量m_instance,这是一个MyClass类的指针变量,MyClass类的所有对象都共享这一个指针。
  往后一看,没有别的必要的成员变量了,显然这个指针变量就是我们实现单例类的核心元素
  继续看,到了公有成员领域,看到两个成员函数,首先这个func函数吧……没有返回值,然后输入一个整形变量a,虽然普通的成员函数可以调用类中的所有成员变量,但是成员变量也就一个指针,说明这整形变量不是拿来给类用的,这函数八成是个测试用的显示函数,不需要去管。
  需要特别关注的是GetInstance()这个函数,它是一个静态成员函数。我们复习一下C++中的相关知识:类中的静态成员函数只能使用静态成员变量,不能使用普通成员变量。这说明了该函数只与静态成员变量m_instance有关,并且该函数的返回值也正是m_instance

  经过上面的分析,接下来就可以分析cpp文件中的内容了:

#include "MyClass.h"
#include 

using namespace std;

MyClass * MyClass::GetInstance()
{
	if (m_instance == nullptr) {//当该指针为空指针时,才会返回新建对象的指针!
		m_instance = new MyClass();
	}
	return m_instance;//返回该指针
}

void MyClass::func(int a)//测试用的函数
{
	cout << "test!——" << a << endl;
}

MyClass *MyClass::m_instance = nullptr;//静态成员变量需要在类的作用域外初始化

  经过对头文件的分析,源文件里面的内容就相对好理解了。我们直接锁定核心函数GetInstance(),在这个函数中其实只做了一次判断:如果m_instance是个空指针,就新建一个实例对象,并且用m_instance指向该实例对象,并将其返回。
  如果m_instance已经不是空指针了,它已经指向了一个实例对象了,那会如何?显然,函数就不会进行到m_instance = new MyClass();,也就是说构造函数再也不会被调用了,类的单例化就实现了。
  此外还需要注意下,在C++中静态成员变量必须要在类的作用域外面初始化,因此初始化语句在cpp中必须写在类成员函数之外。
  我们还可以根据单例类的写法,来推测出单例类实例化的方式:

MyClass *pa = MyClass::GetInstance();
pa->func(1);

  首先,单例类使用指针指向对象,所以程序中也只能使用对象指针的方式来创建实例对象。此外,GetInstance()是一个静态成员函数,因此在程序里可以不依托对象的存在而直接调用!
  之后我们可以写无数个实例化单例类的程序语句,但实际上这些指针都指向的同一个对象,这些指针创建时虽然都进入了GetInstance()函数,但是它们无法进入条件判断语句块中,所以也无法新建实例对象。

多线程下的单例模式

存在的问题

  多线程下的单例模式拥有巨大漏洞,我们可以细品这里的代码:

	if (m_instance == nullptr) {//位置1
		m_instance = new MyClass();//位置2
	}

  在思考多线程情况时,我们可以将多线程条件抽象为“有两个线程,在并发执行”。也就是只思考两个线程的情况。
  假设有线程a和线程b,在一起工作。
  首先,还没有实例化对象,因此线程a和b可能同时到了位置1,也就是都进入了条件判断语句块(毕竟没有实例化对象,大家都满足条件)

if (m_instance == nullptr){
	//线程a和b都到这里了
}

  假设这个时候线程a的时间片用光了,要进入就绪状态了,于是线程a在位置1暂时停了下来。
  这个时候切换到线程b,线程b进入了运行态(多线程嘛,轮着干活呗),线程b一路向下走,完成了实例化,这样就已经新建了一个对象,并假设线程b已经离开了这个函数。

	if (m_instance == nullptr) {//线程a暂时停在这里
		m_instance = new MyClass();//线程b执行完了这句程序,已经完成了实例化。
	}//线程b暂时停在这里

  其实到这里问题就已经暴露出来了,不过我们还是把这个问题讲到最后吧。
  这个时候,线程a从就绪态进入了运行态,继续往下走(他不知道线程b干了啥,毕竟轮着干活,理论上也是各干各的)。于是线程a走到了位置2,并再次进行了一次实例化,悲剧就发生了。

	if (m_instance == nullptr) {
		m_instance = new MyClass();//线程a执行完了这句程序,又来了一次实例化!
	}//线程b暂时停在这里

  可见,这一块代码:

if (m_instance == nullptr) {//位置1
		m_instance = new MyClass();//位置2
	}

  在多线程里应该是个临界区域,也就是每次只能有一个线程进入,显然,如果每次只有一个线程进入该代码区域,就能完美实现单例类的功能。
  说到保证每次只让一个线程进入代码块,我互斥量就要开始表演了。

基础解决方案

  基础方案就是加个互斥量。

#include 
std::mutex resource_mutex;//新建一个互斥量
MyClass * MyClass::GetInstance()
{
	std::unique_lock<std::mutex> myMutex(resource_mutex);//上锁
	if (m_instance == nullptr) {//当该指针为空指针时,才会返回新建对象的指针!
		m_instance = new MyClass();
	}
	return m_instance;//返回该指针
}

  直接对该代码块进行加锁,这样每次都能保证只会有一个线程进入该代码块。

提高效率的方案

  我们回顾一下刚才的方案。

#include 
std::mutex resource_mutex;//新建一个互斥量
MyClass * MyClass::GetInstance()
{
	std::unique_lock<std::mutex> myMutex(resource_mutex);//上锁
	if (m_instance == nullptr) {//当该指针为空指针时,才会返回新建对象的指针!
		m_instance = new MyClass();
	}
	return m_instance;//返回该指针
}

  我们去想一下,单例类对象是如何工作的:

MyClass *pa = MyClass::GetInstance();
pa->func(1);
MyClass *pb = MyClass::GetInstance();
pa->func(2);
MyClass *pc = MyClass::GetInstance();
pa->func(3);

  从上面的代码中可以看到,每个对象都需要调用一次GetInstance(),这样每个对象都需要进行一次同步,也就是访问一下锁。
  实际上,我们需要防范的场景是第一个对象还没被创建时的多线程竞争问题,一旦第一个实例对象被创建完毕,就不再需要互斥量对代码上锁了。
  不仅如此,如果实例对象创建完毕后,每个对象指针建立时都会访问一下互斥量,这样无形中增加了额外的工作量,影响了代码的性能。
  因此我们的优化目的是:当唯一的对象被创建后,其他线程不会去访问互斥量。
  优化方案:双重检测

td::mutex resource_mutex;//新建一个互斥量
MyClass * MyClass::GetInstance()
{
	if (m_instance == nullptr){//先判定一次
		std::unique_lock<std::mutex> myMutex(resource_mutex);//上锁
		if (m_instance == nullptr) {//当该指针为空指针时,才会返回新建对象的指针!
			m_instance = new MyClass();
		}
	}
	return m_instance;//返回该指针
}

  在上锁语句前再增加一次检测,这样等到唯一的实例对象被创建后,其余的线程不会再去访问互斥量,提高了程序运行效率。

你可能感兴趣的:(C++,c++)