C++编程之单例模式

目录

1.线程安全

2.线程不安全的示例

2.1示例一:

2.2示例二:

3.饿汉模式与懒汉模式的区别


所谓的单例模式,指的是类在内存中有且仅有一个对象。

1.线程安全

所谓线程安全指的是:当我们的整个程序中创建了多个线程,而这多个线程有可能运行同一段代码,如果每一次多线程程序运行这段代码的结果和单线程程序运行这段代码的结果一样,其变量值和预期一样,那么这个多线程的程序就是线程安全的。

示例:

主函数创建两个线程,使两个线程调用Object类里面的静态方法,并打印静态方法返回的对象地址

#include <iostream>
#include <thread>
using namespace std;
//1、线程安全的情况
class Object
{
private:
	int value;
	static Object instance;//静态变量需要类外初始化
	static int num;
private:
	Object(int x = 0) :value(x) {}//构造函数设为私有,外部函数无法访问
	Object(const Object& obj) = delete;//删除拷贝构造
	Object& operator=(const Object& ob) = delete;//删除等号运算符重载
public:
	static Object& GetInstance()//因为删除了拷贝构造,所以只能以引用或者指针的形式返回这个静态对象本身
	{
		return instance;
	}
};
Object Object::instance(10);
void funa()
{
	Object& obja = Object::GetInstance();//调用静态方法返回静态对象本身
	cout << &obja << endl;
}
void funb()
{
	Object& objb = Object::GetInstance();//调用静态方法返回静态对象本身
	cout << &objb << endl;
}
int main()
{
	thread thra(funa);
	thread thrb(funb);

	thra.join();
	thrb.join();

	return 0;
}

程序运行结果:

由此上述代码执行之后,打印出来的两个对象地址是一样的。

解释:

静态对象instance在进入主函数之前就已经在数据区构造完成,并设置其value=10,然后进入主函数之后,无论是thra线程,还是thrb线程,都只能去访问这一个静态对象instance,并不会对其做出任何改变,所以是线程安全的方式。

2.线程不安全的示例

通过对下面的示例讲解线程不安全的两个情况,并解释什么是饿汉模式,什么是懒汉模式。

2.1示例一:

在解释饿汉模式之前,先了解一下静态变量在程序运行过程中是如何初始化的,这方便对后续的示例进行理解。

程序对静态量用字面常量和变量在底层初始化是不一样的。

示例:

void funa(int x)
{
    static int a=10;
}
int main()
{
     funa(10);
}

静态常量:在进入主函数之前遇到static int a=10,就会把a这个变量放在数据区,并开辟一定大小的空间,然后赋初值10(这是线程安全的)。

示例:

void funb(int x)
{
    static int b=x;
}
int main()
{
    funb(10);
}

静态变量:进入主函数之前遇到static int b=x;在数据区先给这个变量b分配内存,由于并不知道具体赋值为多少,所以先不赋值,此时静态变量内部会有一个标志fig=0,表示没有赋值。在主函数之后调用funb(10),再去数据区赋值,此时fig标志位从0变为1,表示已经赋初值。所以有变量的时候数据区是先修改标记fig,再修改改值(这是线程不安全的,因为如果程序存在多个线程竞争去改变数据区这个变量的标志位fig)。

通过对程序处理静态变量的理解之后,在看看下面的示例:

主函数创建两个线程,这两个线程分别调用Objec类里面的静态方法,并给x传参,然后构造一个静态对象并返回此对象。

//饿汉模式
class Object
{
public:
	int value;
private:
	Object(int x = 0) :value(x) {}//构造函数设为私有,外部函数无法访问
	Object(const Object& obj) = delete;//删除拷贝构造
	Object& operator=(const Object& ob) = delete;//删除等号运算符重载
public:
	static Object& GetInstance(int x)//因为删除了拷贝构造,所以只能以引用或者指针的形式返回这个静态对象本身
	{
		//直接在静态函数中构建这个静态对象并返回其本身
		static Object instance(x);
		return instance;
	}
};
void funa()
{
	Object& obja = Object::GetInstance(10);//调用静态方法返回静态对象本身
	cout << obja.value << endl;
	cout << &obja << endl;
}
void funb()
{
	Object& objb = Object::GetInstance(20);//调用静态方法返回静态对象本身
	cout << objb.value << endl;
	cout << &objb << endl;
}
int main()
{
	thread thra(funa);
	thread thrb(funb);

	thra.join();
	thrb.join();

	return 0;
}

程序运行结果:

 第一次运行:

第二次运行:

由此可见上述的示例不是线程安全的,我们本来期望的是thra线程给x传参为10,构建一个静态对象使其value=10,然后返回这个静态对象。thrb线程给x传参为20,然后构建一个静态对象使其value=20,并返回这个静态对象。但是程序运行结果是只有一个对象,并且value的值是不确定的,并不是我们预期的结果。

原因:

static Object instance(x);这一句会在进入主函数之前就已经在数据区为其分配了地址空间,进入主函数之后启动线程,两个线程同时运行Object类里面的静态方法(静态方法只有一份),就存在资源竞争的问题,争夺标记fig从0变为1的权利,哪个线程抢到机会,就会将自己的值赋值给value,那么没有抢到的那个线程再去访问时发现fig标志已被设置为1,表明已经被赋值了,所以它就只能退出这个静态方法。

最后来说说什么是饿汉模式,通过上面构建静态对象的例子可知,饿汉模式就是我们设计的类只要开始加载,就必须把单例初始化完成,从而保证多线程在访问这一段唯一资源时,单例是已经存在的(拿上面的例子来说:就是Object这个类一旦开始加载,static Object instance(x)这个对象是需要赶紧初始化的,从而保证多个线程在调用这个唯一的静态方法时,静态对象已经存在,不需要线程做其他的改变)。

2.2示例二:

示例:

主函数启动两个线程,两个线程分别调用Object类里面的静态方法,在静态指针pobj为空的时候,从堆区构建一个Object的对象,然后返回pobj这个静态的对性指针,此指针会指向堆区构建的Object对象,然后两个线程分别打印这个指针所指对象的地址。

class Object
{
public:
	int value;
	static Object* pobj;
private:
	Object(int x = 0) :value(x) {}//构造函数设为私有,外部函数无法访问
	Object(const Object& obj) = delete;//删除拷贝构造
	Object& operator=(const Object& ob) = delete;//删除等号运算符重载
public:
	static Object* GetInstance()//因为删除了拷贝构造,所以只能以引用或者指针的形式返回这个静态对象本身
	{
		if (pobj == NULL)
		{
			std::this_thread::sleep_for(std::chrono::milliseconds(10));
			pobj = new Object(10);
		}
		return pobj;
	}
};
Object* Object::pobj = NULL;
void funa()
{
	Object* pa = Object::GetInstance();
	cout << pa << endl;
}
void funb()
{
	Object* pb = Object::GetInstance();
	cout << pb  << endl;
}
int main()
{
	thread thra(funa);
	thread thrb(funb);

	thra.join();
	thrb.join();

	return 0;
}

程序运行结果:

第一次运行:

第二次运行:

通过程序运行结果可见两个线程返回的对象地址并不是一样的,但是我们期望的线程安全情况下多线程执行同一段程序,其返回的对象应该只有一个,地址是唯一的。出现现在这种情况的原因是:

两个线程都进入静态方法GetInstance()里面,都发现pobj指针为空,于是都进入If语句内,然后我设置了一个睡眠时间,然后一个执行pobj = new Object(10);创建了一个对象给pobj,并返回打印这个对象的地址,随后另外一个线程又执行pobj = new Object(10);;又创建了一个对象重新给pobj赋值,然后打印对象的地址。

所以通过上述例子可见:所谓的懒汉模式就是指在我们设计的类开始加载的时候并不需要赶紧初始化单例,而是等到第一次被使用时才初始化(所以才说它是懒汉)。对于懒汉模式如果想要保证线程安全,只创建一个对象,可以对创建对象的那部分代码加锁。

修改后代码如下:
 

#include <mutex>
std::mutex mtx;
class Object
{
public:
	int value;
	static Object* pobj;
private:
	Object(int x = 0) :value(x) {}//构造函数设为私有,外部函数无法访问
	Object(const Object& obj) = delete;//删除拷贝构造
	Object& operator=(const Object& ob) = delete;//删除等号运算符重载
public:
	static Object* GetInstance()//因为删除了拷贝构造,所以只能以引用或者指针的形式返回这个静态对象本身
	{
		std::lock_guard<std::mutex>lock(mtx);
		if (pobj == NULL)
		{
			std::this_thread::sleep_for(std::chrono::milliseconds(10));
			pobj = new Object(10);
		}
		return pobj;
	}
};
Object* Object::pobj = NULL;
void funa()
{
	Object* pa = Object::GetInstance();
	cout << pa << endl;
}
void funb()
{
	Object* pb = Object::GetInstance();
	cout << pb  << endl;
}
int main()
{
	thread thra(funa);
	thread thrb(funb);

	thra.join();
	thrb.join();

	return 0;
}

3.饿汉模式与懒汉模式的区别

(1)饿汉模式:

在类加载的时候就已经创建好对象,程序在调用的时候会直接获取先前已经构建好的实例对象。

这种方式由于构建好的对象是静态的,会一直占用数据区的空间,但是调用的时候节省了构建对象的时间。

(2)懒汉模式:

在访问静态方法的时候才去构建对象。

这种方式是在需要使用的时候才构建,在一定程度上节省了空间,避免对象一直在内存浪费空间,但是用的时候在构建,会消耗一定的时间,使得程序的速度变慢。

这种模式不安全,需要加锁进行控制。

猜你喜欢

转载自blog.csdn.net/m0_54355780/article/details/123188535