单例模式

1 核心思想

单例模式确保一个类只有一个实例,并提供一个全局访问点。

单例模式产生的对象具有全局变量的特点,在任何位置都可以通过接口获取到那个唯一实例;

具体运用场景如:

设备管理器,系统中可能有多个设备,但是只有一个设备管理器,用于管理设备驱动;
数据池,用来缓存数据的数据结构,需要在一处写,多处读取或者多处写,多处读取;

2 单例模式的实现

2.1 饿汉式

饿汉式就是静态成员直接初始化,这样就保证了静态变量只被初始化一次,可以确保多个线程使用的是同一个单例对象。
缺点是在不使用单例的时候也需要进行初始化,内存占用较大。

class Singleton
{
private:
	Singleton() { std::cout << "构造对象" << std::endl; };
	~Singleton() { std::cout << "析构对象" << std::endl; };
    Singleton(Singleton&) = delete;
	Singleton& operator=(const Singleton&) = delete;
	
	static Singleton* m_pSig;

public:
	static Singleton* GetInstance()
	{
		return m_pSig;
	}

};

Singleton* Singleton::m_pSig = new Singleton;

int main()
{
	Singleton* pSing1 = Singleton::GetInstance();
	Singleton* pSing2 = Singleton::GetInstance();

	std::cout << "pSing1:" << "\t" << pSing1 << "\tpSing2:" << "\t" << pSing2 << std::endl;
	return 0;
}

在这里插入图片描述

2.2 有缺陷的懒汉式

懒汉式表示用到的时候才去实例化,也就是说只有调用GetInstance函数的时候才会产生对象,好处是如果没有使用就不会占用内存,缺点是必须仔细进行访问控制以应对多线程访问。

//有多线程访问隐患的懒汉式单例模式
class Singleton
{
private:
	Singleton() { std::cout << "构造对象" << std::endl; };
	~Singleton() { std::cout << "析构对象" << std::endl; };
	Singleton(Singleton&) = delete;
	Singleton& operator=(const Singleton&) = delete;

	static Singleton* m_pSig;

public:
	static Singleton* GetInstance()
	{
		if(m_pSig == nullptr)
			m_pSig = new Singleton;
		return m_pSig;
	}

	static void Delete()
	{
		delete m_pSig;
		m_pSig = nullptr;
	}
};

Singleton* Singleton::m_pSig = nullptr;

int main()
{
	Singleton* pSing1 = Singleton::GetInstance();
	Singleton* pSing2 = Singleton::GetInstance();

	std::cout << "pSing1:" << "\t" << pSing1 << "\tpSing2:" << "\t" << pSing2 << std::endl;
	return 0;
}

在GetInstance函数中若m_pSig为空,则new一个新的对象出来,若不为空则直接返回该对象。在单线程访问的情况下,可以保证取得正确的结果。如下图所示:
在这里插入图片描述
但在多线程访问的情况下,有可能会出现创建了两个对象。比如第一个线程在GetInstance函数中已经执行了m_pig是否为空的if判断,此时需要进行new操作,但在操作之前,第二个线程开始执行GetInstance函数,因为此时m_pig尚为空,第二个线程进行了new的操作并进行了返回创建了第一个对象。等到第一个线程开始执行的时候,因为它会继续暂停之前的操作,所以直接new了一个新的对象并返回,也就是又创建了一个新的对象。这样就造成了两个线程分别创建了不同的对象,单例的设计也就无效了。如下面的代码所示:

static Singleton *pSing1,*pSing2;

void ThreadFunc1()
{
	pSing1 = Singleton::GetInstance();
}

void ThreadFunc2()
{
	pSing2 = Singleton::GetInstance();
}

int main()
{
	int index = 0;
	while (1)
	{
		//Singleton* pSing1 = Singleton::GetInstance();
		//Singleton* pSing2 = Singleton::GetInstance();

		std::thread* pThread1 = new std::thread(ThreadFunc1);
		std::thread* pThread2 = new std::thread(ThreadFunc2);

		std::this_thread::sleep_for(std::chrono::milliseconds(100));

		if (pSing1 && pSing2)
		{
			if (pSing1 != pSing2)
				break;
			else
			{
				Singleton::Delete();
				index++;
				continue;
			}
		}
			
	}
	
	std::cout << index << std::endl;
	std::cout << "pSing1:" << "\t" << pSing1 << "\tpSing2:" << "\t" << pSing2 << std::endl;
	return 0;
}

在这里插入图片描述

2.3 双重检查解决懒汉模式的缺陷

//使用双重锁解决懒汉式单例模式的缺陷
class Singleton
{
private:
	Singleton()
	{
		std::cout << "构造对象" << std::endl;
	};

	~Singleton()
	{
		std::cout << "析构对象" << std::endl;
	};

	Singleton(Singleton&) = delete;
	Singleton& operator=(const Singleton&) = delete;

	static Singleton* m_pSig;
	static std::mutex m_lock;

public:
	static Singleton* GetInstance()
	{
		if (m_pSig == nullptr)
		{
			std::lock_guard<std::mutex> lk(m_lock);

			if(m_pSig == nullptr)
				m_pSig = new Singleton;
		}
			
		return m_pSig;
	}

	static void Delete()
	{
		delete m_pSig;
		m_pSig = nullptr;
	}
};

Singleton* Singleton::m_pSig = nullptr;
std::mutex Singleton::m_lock;

//下面是main函数
static Singleton *pSing1,*pSing2;

void ThreadFunc1()
{
	pSing1 = Singleton::GetInstance();
}

void ThreadFunc2()
{
	pSing2 = Singleton::GetInstance();
}

int main()
{
	int index = 0;
	while (1)
	{
		//Singleton* pSing1 = Singleton::GetInstance();
		//Singleton* pSing2 = Singleton::GetInstance();

		std::thread* pThread1 = new std::thread(ThreadFunc1);
		std::thread* pThread2 = new std::thread(ThreadFunc2);

		std::this_thread::sleep_for(std::chrono::milliseconds(100));

		if (pSing1 && pSing2)
		{
			if (pSing1 != pSing2)
				break;
			else
			{
				Singleton::Delete();
				index++;
				continue;
			}
		}
			
	}
	
	std::cout << index << std::endl;
	std::cout << "pSing1:" << "\t" << pSing1 << "\tpSing2:" << "\t" << pSing2 << std::endl;
	return 0;
}

代码运行测试可以发现永远只生成一个对象实例。

核心的改变就在于GetInstance函数中使用了双重检查,即

		if (m_pSig == nullptr)
		{
			std::lock_guard<std::mutex> lk(m_lock);

			if(m_pSig == nullptr)
				m_pSig = new Singleton;
		}

第一次进行m_pSig是否为空的判断是为了避免每次调用GetInstance函数都加锁,这样会造成开销太大,加上m_pSig是否为空的判断就会只有第一次调用GetInstance函数的时候才加锁。加锁之后再次进行m_pSig是否为空的判断,若为空则实例化对象。

双重检查机制在一般情况下是安全的,但要求编译器是按照顺序执行代码的,但实际上这里可能有内存安全的bug

m_pSig = new Singleton;

为了执行这句代码,机器需要做三样事儿:

1.singleton对象分配空间。

2.在分配的空间中构造对象

3.使m_pSig指向分配的空间

遗憾的是编译器并不是严格按照上面的顺序来执行的。可以交换2和3.

将上面三个步骤标记到代码中就是这样:

Singleton* Singleton::GetInstance()
 {
    if (m_pSig == nullptr) {
       std::lock_guard<std::mutex> lk(m_lock);
       
        if (m_pSig == nullptr) 
        {
            m_pSig = // Step 3
            operator new(sizeof(Singleton)); // Step 1
            new (m_pSig) Singleton; // Step 2
        }

    }
    return m_pSig;
}

线程A进入了GetInstance函数,并且执行了step1和step3,然后挂起。这时的状态是:m_pSig不为空,而m_pSig指向的内存区没有对象!线程B进入了GetInstance函数,发现m_pSig不为null,就直接return m_pSig了。

2.4 推荐单例模式实现方式:使用局部静态变量

//使用局部静态变量

class Singleton
{
private:
	Singleton()
	{
		std::cout << "构造对象" << std::endl;
	};

	~Singleton()
	{
		std::cout << "析构对象" << std::endl;
	};

	Singleton(Singleton&) = delete;
	Singleton& operator=(const Singleton&) = delete;

public:
	static Singleton& GetInstance()
	{
		static Singleton value;
		return value;
	}
};

之所以可以这样做是因为:局部静态变量不仅只会初始化一次,而且还是线程安全的。

注意GetInstance函数现在返回的是引用

这种单例被称为Meyers’ Singleton。这种方法很简洁,也很完美,但是注意:

  • gcc 4.0之后的编译器支持这种写法。
  • C++11及以后的版本(如C++14)的多线程下,正确。
  • C++11之前不能这么写。

2.5 使用c++11的call_once

#include <iostream>
#include <thread>
#include <mutex>

std::once_flag flag;

class Singleton
{
private:
	Singleton()
	{
		std::cout << "构造对象" << std::endl;
	};


	Singleton(Singleton&) = delete;
	Singleton& operator=(const Singleton&) = delete;

	static std::unique_ptr<Singleton> m_pSig;

public:
	static Singleton& GetInstance()
	{
		std::call_once(flag, []() {m_pSig.reset(new Singleton()); });
		return *m_pSig;
	}
};


std::unique_ptr<Singleton> Singleton::m_pSig;

void do_onceflag()
{
	Singleton& s = Singleton::GetInstance();
	std::cout << &s << std::endl;
}

int main()
{
	std::thread t1(do_onceflag);
	std::thread t2(do_onceflag);

	t1.join();
	t2.join();

	return 0;
}

3 总结

单例模式思想简单,但写出完全无风险的代码还是需要认真分析的,在满足前提条件时推荐使用2.4的方式进行实现。

参考:
https://zhuanlan.zhihu.com/p/62014096
https://www.cnblogs.com/sunchaothu/p/10389842.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值