多线程下的单例模式实现

     单实例设计模式是我们平常接触最多的一种设计模式,它的特点在于保证一个类只有一个实例,并且对类成员的访问都是通过一个全局访问点来进行的。单实例主要用在整个场景中只能有一个该操作类对象,不允许再有其他的该操作类对象,比如:Http传输管理类,线程池管理类等。下面我们对单线程和多线程不同模式下的单实例实现分别进行分析。

一,单线程下的单实例

     单例模式的结构图如下:
在这里插入图片描述
     为了避免在外部创建类对象,我们需要将单例类的构造函数设置为私有,并且提供一个静态的接口来创建唯一实例对象。单例模式根据创建实例的时机又可以分为饿汉模式和懒汉模式。

1, 懒汉模式

     懒汉模式即当需要的时候才会去new对象,代码实现如下:

class Singelton
{
private:
	Singelton() {}
	static Singelton* s_singleton;

public:
	static Singelton* GetInstance()
	{
		if (s_singleton == NULL)
		{
			s_singleton = new Singelton();
		}
		return s_singleton;
	}
};
Singelton* Singelton::s_singleton = NULL;//注意静态变量类外初始化

     当多个线程同时调用GetInstance()时,由于内部的new操作并没有加锁,会出现以下两种情况:
A, 多个线程同时对一个变量进行写操作,地址访问冲突,导致程序崩溃;
B, 有一个临界点,同时new了两个对象,违背了单实例的原则,返回的类指针不唯一相同。
     因此,懒汉模式在是多线程不安全的。

2, 饿汉模式

     饿汉模式即在调用接口获取对象之前直接先new一个对象,代码实现如下:

class Singelton
{
private:
	Singelton() {}
	static Singelton* s_singleton;

public:
	static Singelton* GetInstance()
	{
		return s_singleton;
	}
};
Singelton* Singelton::s_singleton = new Singelton;//注意静态变量类外初始化

     由于饿汉模式类对象的创建是在一个静态变量初始化时new出来的,那么在多线程中调用GetInstance()并不会有问题(全局变量和静态变量是在main函数之前初始化的)

二,多线程下的单实例

     针对上述两种单实例模式的实现,可以知道:饿汉模式是线程安全的,懒汉模式是线程不安全的。下面我们来实现线程安全的懒汉模式。

1,加锁

     通过锁来实现,在每次new之前加锁,new完成后释放锁,保证永远只有一个线程对该静态类指针进行赋值操作,代码如下:

#include <mutex>

class Singelton
{
private:
	Singelton() {}
	static Singelton* s_singleton;
	static std::mutex s_cvMutex; // 互斥锁.

public:
	static Singelton* GetInstance()
	{
		if (s_singleton == nullptr)
		{
			std::lock_guard<std::mutex> lck(s_cvMutex);
			if (s_singleton == nullptr)
			{
				s_singleton = new Singelton;
			}		
}
		return s_singleton;
	}
};
Singelton* Singelton::s_singleton = nullptr;//注意静态变量类外初始化
std::mutex Singelton::s_cvMutex;

     这里有一点需要注意,在加锁之后,new之前,又加了一重判空操作,这一步是为了保证多个线程同时进入了第一层判空语句中,当一个线程new完之后,如果不加第二重判空,那么第二个线程会再次进行new操作,导致实例不唯一。

2,原子操作

     单纯的原子操作并没有锁的功能,需要配合上:if + while + Sleep(当然,也可以说是if + while,不去Sleep也可以),具体代码实现如下:

class Singelton
{
private:
	Singelton() {}

public:
	static Singelton* GetInstance()
	{
		if (g_singleton == NULL)
		{
			long pre_value = ::InterlockedExchange(&g_nLock, 1); //返回原来的值
			if (pre_value != 0)
			{
				while (g_singleton == NULL)
				{
					::Sleep(50);
				}
			}
			if (g_singleton == NULL)
			{
				g_singleton = new Singelton;
			}
		}
		return g_singleton;
	}

};
Singelton* volatile g_singleton = nullptr;
long volatile g_nLock = 0;

     多个线程同时调用InterlockedExchange,只能有一个线程得到0,保证只初始化一次,其余线程进入while循环等待,直到g_singleton非空。
     大家可以注意到,这里还用到了volatile关键字,volatile一般有两个好处:1是使得多个线程直接操作内存,变量被某个线程改变后其它线程也可以及时看到改变后的值;2是阻止编译器优化操作volatile变量的指令执行顺序。这里如果不使用它,就可能导致编译器调整汇编指令的顺序,分配完内存就直接把地址赋值给g_singleton指针,后面再调用构造函数,它这样调整的理由可能是这样子:分配到的内存指针在后续的执行中没有被修改,先赋值给g_singleton和晚赋值给g_singleton没有区别,这就导致了半成品对象的产生。

发布了78 篇原创文章 · 获赞 79 · 访问量 6万+

猜你喜欢

转载自blog.csdn.net/bajianxiaofendui/article/details/87010713