【C++】单例模式及线程安全

单例模式及其下线程安全

1.设计模式

设计模式:
设计模式(Design Pattern)**是一套被反复使用、多数人知晓的、经过分类的、代码设计经验的总结。**为什么会产生设计模式这样的东西呢?就像人类历史发展会产生兵法。最开始部落之间打仗时都是人拼人的对
砍。后来春秋战国时期,七国之间经常打仗,就发现打仗也是有套路的,后来孙子就总结出了《孙子兵法》。孙子兵法也是类似。
使用设计模式的目的:为了代码可重用性、让代码更容易被他人理解、保证代码可靠性。 设计模式使代码编
写真正工程化;设计模式是软件工程的基石脉络,如同大厦的结构一样。

1.1单例模式

单例模式:
一个类只能创建一个对象,即单例模式,该模式可以保证系统中该类只有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。比如在某个服务器程序中,该服务器的配置信息存放在一个文件
中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息,这种方式简化了在复杂环境下的配置管理。
单例模式有两种实现模式:

2.饿汉模式

理解:

就是说不管你将来用不用,程序启动时就创建一个唯一的实例对象。

单例(唯一的实例对象)
实现:
1.与实现唯一抽象类原理相同,通过私有化构造函数和析构函数实现单例(唯一对象)
2.通过static关键字,实现访问其私有成员并创建对象

//单例:
//饿汉 --> 一上来就初始化,main函数开始之间就实例化对象
class Singleton
{
public:
	static Singleton& GetInstance()
	{
		return _sInst;
	}
private:
	//构造函数私有化
	Singleton()
	{}
	//拷贝构造函数私有化
	Singleton(const Singleton&) = delete;
	static Singleton _sInst;//声明
};
Singleton Singleton::_sInst;
int main()
{
	cout << &Singleton::GetInstance() << endl;
	cout << &Singleton::GetInstance() << endl;
	cout << &Singleton::GetInstance() << endl;
	cout << &Singleton::GetInstance() << endl;
	system("pause");
	return 0;
}


通过地址查看是否使用饿汉模式实现单例

饿汉模式多线程下是否安全

验证:

int main()
{
	/*cout << &Singleton::GetInstance() << endl;
	cout << &Singleton::GetInstance() << endl;
	cout << &Singleton::GetInstance() << endl;
	cout << &Singleton::GetInstance() << endl;*/

	// 多线程来调用Singleton::GetInstance(),有没有线程安全的问题?
	// 
	vector<thread> vthreads;
	for (size_t i = 0; i < 4; ++i)
	{
		vthreads.push_back(thread([]()
		{
			this_thread::sleep_for(std::chrono::seconds(1));
			for (size_t i = 0; i < 100; ++i)
			{
				cout << &Singleton::GetInstance() << endl;
			}
		}));
	}
	for (auto& e : vthreads)
	{
		e.join();
	}
	system("pause");
	return 0;
}

在这里插入图片描述
可以看到是没有问提的
饿汉模式下是没有线程安全问题的,因为它在mian()函数调用之间,已经实例化了对象,main()调用后多线程才会进入,所以不存在线程安全问题。

饿汉模式优缺点

优点:实现简单,不存在线程安全的问题
缺点:在main函数之前初始化实例,如果程序中单例较多,程序启动慢,性能过低。其次如果两个单例类有依赖关系,无法保证创建初始化实例的顺序。

3.懒汉模式(常用)

理解:

使用时才加载资源,第一次使用实例对象时,创建对象。进程启动无负载。多个单例实例启动顺序自由控制。

概念:如果单例对象构造十分耗时或者占用很多资源,比如加载插件啊, 初始化网络连接啊,读取文件啊等
等,而有可能该对象程序运行时不会用到,那么也要在程序一开始就进行初始化(饿汉模式),就会导致程序启动时非常的缓慢。 所以这种情况使用懒汉模式(延迟加载)更好。
实现:
1.同理,私有化构造函数和拷贝构造函数,实现单例
2.static关键字修饰函数变量,访问私有成员
与饿汉模式不同,在于懒汉模式在类中封装的是对象指针,所以static初始化的是指针类型。

class Singleton
{
public:
	static Singleton& GetInstance()
	{
		if (_sInst == nullptr)
		{
			_sInst = new Singleton;
		}
		return *_sInst;
	}
private:
	//构造函数私有化
	Singleton()
	{}
	//拷贝构造函数私有化
	Singleton(const Singleton&) = delete;
	static Singleton* _sInst;//声明
};
Singleton* Singleton::_sInst = nullptr;
int main()
{
	cout << &Singleton::GetInstance() << endl;
	cout << &Singleton::GetInstance() << endl;
	cout << &Singleton::GetInstance() << endl;
	cout << &Singleton::GetInstance() << endl;
	system("pause");
	return 0;
}

调用GetInstance()接口申请空间实例化指针所指向空间,后面进程再进来时无需再次申请空间,提高性能

懒汉模式在多线程下是否线程安全

代码测试:

int main()
{
	/*cout << &Singleton::GetInstance() << endl;
	cout << &Singleton::GetInstance() << endl;
	cout << &Singleton::GetInstance() << endl;
	cout << &Singleton::GetInstance() << endl;*/

	// 多线程来调用Singleton::GetInstance(),有没有线程安全的问题?
	// 
	vector<thread> vthreads;
	for (size_t i = 0; i < 100; ++i)
	{
		vthreads.push_back(thread([]()
		{
			this_thread::sleep_for(std::chrono::seconds(1));
			for (size_t i = 0; i < 10; ++i)
			{
				cout << &Singleton::GetInstance() << endl;
			}
		}));
	}
	for (auto& e : vthreads)
	{
		e.join();
	}
	system("pause");
	return 0;
}

在这里插入图片描述
饿汉模式在多线程下不是线程安全的,如上图但这是一个随机性bug,并不出每次都会出现

如何解决懒汉模式在多线程下的线程安全问题

问题原因:
假设有多线程场景下,第一个线程进入时指针为空申请空间,但这个线程如果还没来得及退出,第二个线程进来判断时,指针仍为空,再次申请空间,导致无法实现单例模式下的线程安全。
解决方法:
加锁,若指针为空,第一个线程进入后,加锁操作,其他线程均被阻塞,无法进入,当第一个线程资源空间申请后解决,其他线程再进入,就可以解决。

class Singleton
{
public:
	static Singleton& GetInstance()
	{ 
		_stx.lock();
		if (_sInst == nullptr)
		{
			_sInst = new Singleton;
		}
		_stx.unlock();
		return *_sInst;
	}
private:
	//构造函数私有化
	Singleton()
	{}
	//拷贝构造函数私有化
	Singleton(const Singleton&) = delete;
	static Singleton* _sInst;//声明 
	static mutex _stx;
};
Singleton* Singleton::_sInst = nullptr;
mutex Singleton::_stx;

可多次运行,看是否解决
在这里插入图片描述
此时已经解决懒汉模式的线程安全问题。
优化:
每次线程进入时都要进行加锁解锁操作,由于锁调度也会耗费资源影响性能。而且也只有在指针为空时存在线程安全的问题,所以我们可以再加一层判断,若指针为空则加锁,若不为空则不用加锁
在这里插入图片描述
懒汉模式的完整优化代码

class Singleton
{
public:
	static Singleton& GetInstance()
	{ 
		//双重检查提高效率
		if (_sInst == nullptr)
		{
		    _stx.lock();
			if (_sInst == nullptr)
			{
				_sInst = new Singleton;
			}
			_stx.unlock();
			
		}
		return *_sInst;
	}
private:
	//构造函数私有化
	Singleton()
	{}
	//拷贝构造函数私有化
	Singleton(const Singleton&) = delete;
	static Singleton* _sInst;//声明 
	static mutex _stx;
};
Singleton* Singleton::_sInst = nullptr;
mutex Singleton::_stx;
int main()
{
	/*cout << &Singleton::GetInstance() << endl;
	cout << &Singleton::GetInstance() << endl;
	cout << &Singleton::GetInstance() << endl;
	cout << &Singleton::GetInstance() << endl;*/

	// 多线程来调用Singleton::GetInstance(),有没有线程安全的问题?
	// 
	vector<thread> vthreads;
	for (size_t i = 0; i < 100; ++i)
	{
		vthreads.push_back(thread([]()
		{
			this_thread::sleep_for(std::chrono::seconds(1));
			for (size_t i = 0; i < 10; ++i)
			{
				cout << &Singleton::GetInstance() << endl;
			}
		}));
	}
	for (auto& e : vthreads)
	{
		e.join();
	}
	system("pause");
	return 0;
}

懒汉模式的优缺点

懒汉模式更为常用
优点:是在第一次调用初始化创建实例,不影响程序的启动。其次他调用时创建初始化,顺序可以控制
缺点:实现相对复杂

发布了47 篇原创文章 · 获赞 175 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/qq_43676757/article/details/105459490