多线程(2)-单例模式下的线程安全

什么是线程安全

  简而言之,线程安全问题就是旨在保证高并发的应用场景中,其共有的数据能够按照开发人员所期望的方式进行变化,不会出现差错或异常情况。比如用1000个线程,其中每个线程都对全局变量i进行加一操作,我们期望在所有线程运行结束后,i的值能够被加1000,但是往往事与愿违。

如何保证线程安全

请参照我的上一篇博客:多线程(1)-线程及线程安全

单例模式下的线程安全

原始单例

  单例模式作为最简单的设计模式,也是使用频率最高的,其目的是保证作为一个类对象在整个程序运行周期只能有存在一个。而我们知道单例模式在前人的总结下,分为饿汉模式和懒汉模式,其中饿汉模式作为能够保证实现线程安全的一种实现方法,其核心就是在静态变量初始化的时候就构造完成。但是我们在实际的项目开发中,我们在构造一个对象的时候,需要根据不同的环境来构造一个对象,所以就需要用到懒汉模式,首先,懒汉模式的基本代码如下:

class SingletonInstance
{
private:
	SingletonInstance();
public:
	static SingletonInstance *pInstance;
	static SingletonInstance* GetInstance()
	{
		if (pInstance == nullptr)
		{
			pInstance = new SingletonInstance();
		}
		return pInstance;
	}
};

  懒汉模式的思想就是将静态对象的生成推迟到运行期,这就提供了很多可变革的生成过程,比如结合多态可以实现单例和工厂的结合,这里不再赘述。
  回到线程安全的角度上,我们需要考虑高并发的情况下,懒汉模式下的创建过程是否安全。考虑这么个情况,假设存在两个线程A B,同时调用了GetInstance()函数进行获取实例,在某个时刻,线程A运行到pInstance = new SingletonInstance();这句之前,线程发生切换,如下图。
在这里插入图片描述
  此时进入B线程,此时pInstance尚未构造,所以在if(pInstance == nullptr)时,依旧能够进入到if语句块,然后正常运行代码块,进行了pInstance = new SingletonInstance()构造过程,函数完成后,线程B退出,继续运行线程A,由于线程切换的上下文保存的原因,线程A立刻运行pInstance = new SingletonInstance();这条语句。由此,我们发现pInstance被赋值了两次,并new了两块内存,造成了内存的浪费。

加锁单例

  为了解决这种情况,我们很容易的想到在GetInstance代码块中直接锁住,代码如下:

static std::mutex mute;
class SingletonInstance
{
private:
	SingletonInstance();
public:
	static SingletonInstance *pInstance;
	static SingletonInstance* GetInstance()
	{
		mute.lock();
		if (pInstance == nullptr)
		{
			pInstance = new SingletonInstance();
		}
		return pInstance;
		mute.unlock();
	}
};

  这样在线程运行GetInstance并加锁之后,其他线程再运行mute.lock的时候,阻塞并等待解锁,这样就实现了线程安全。

双重检查锁(DLC)

  但是在c++这样一门追求极致性能的语言上,我们在保证线程安全的同时也需要考虑运行效率,在看以上代码,我们在思考一个问题,由于我们在函数体中刚开始的时候就进行了加锁,那么假设GetInstance的调用频率十分高,那是不是意味着mute加锁、解锁的频率也十分高,这就会造成效率的极大降低,所以需要优化这个问题,先上代码:

static std::mutex mute;
class SingletonInstance
{
private:
	SingletonInstance();
public:
	static SingletonInstance *pInstance;
	static SingletonInstance* GetInstance()
	{
		if (pInstance == nullptr)
		{
			std::lock_guard<std::mutex> lock(mute);
			if (pInstance == nullptr)
			{
				pInstance = new SingletonInstance();
			}
		}
		return pInstance;
	}
};

  观察以上代码,我们将mute的加锁解锁放在了if(pInstance ==nullptr)的判是语句中,这样是不是只在未构建pInstance的时候才会进行锁操作,而后的情况将直接返回pInstance,然后为了保证我们刚开始担心的那种线程切换导致的new多个内存空间的情况发生,我们需要在锁的使能语句中再一次判断pInstance是否为空,这种双重判断的情况又被称为DLC(double-checked locking)。
std::lock_guard是模板类,其作用就是防止开发人员在lock之后忘记unlock导致死锁的发生,其实现机理就是在模板类对象的构造函数和析构函数分别进行lock和unlock,更像是一种语法糖,仅此而已。

总结

  单例模式分为饿汉模式和懒汉模式,其中饿汉模式是线程安全的,懒汉模式是非线程安全的,为了保证其安全性,我们需要利用锁来实现,但是直接锁住整个函数体会造成效率的极大降低,比如后面n次的GetInstance中都会进行加锁和解锁操作,这是完全不必要的。所以,我们只需要锁住pInstance的构造过程即可,然后为了防止线程切换导致的new个多个内存空间的恶果,我们需要进行第二次的非空判断。

  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值