【11】c++设计模式——>单例模式

单例模式是什么

在一个项目中,全局范围内,某个类的实例有且仅有一个(只能new一次),通过这个唯一的实例向其他模块提供数据的全局访问,这种模式就叫单例模式。单例模式的典型应用就是任务队列。

为什么要使用单例模式

单例模式充当的就是一个全局变量,为什么不直接使用全局变量呢,因为全局变量破坏类的封装,而且不受保护,访问不受限制。

【11】c++设计模式——>单例模式_第1张图片

饿汉模式

饿汉模式是在类加载时进行实例化的。

#include
using namespace std;

class TaskQueue
{
public:
	TaskQueue(const TaskQueue& obj) = delete;//禁用拷贝构造
	TaskQueue& operator = (const TaskQueue& obj) = delete;//禁用赋值构造
	static TaskQueue* getInstance()  //获取单例的方法
	{
		cout << "我是一个饿汉模式单例" << endl;
		return m_taskQ;
	}

private:
	TaskQueue() = default; //无参构造
	static TaskQueue* m_taskQ; //静态成员需要在类外定义
};
TaskQueue* TaskQueue::m_taskQ = new TaskQueue;//new一个实例;

int main()
{
	TaskQueue* obj = TaskQueue::getInstance();
}

懒汉模式

懒汉模式是在需要使用的时候再进行实例化

#include
using namespace std;

class TaskQueue
{
public:
	TaskQueue(const TaskQueue& obj) = delete;//禁用拷贝构造
	TaskQueue& operator = (const TaskQueue& obj) = delete;//禁用赋值构造
	static TaskQueue* getInstance()  //获取单例的方法
	{
		if (nullptr == m_taskQ)
		{
			m_taskQ = new TaskQueue;
		}
		return m_taskQ;
	}

private:
	TaskQueue() = default; //无参构造
	static TaskQueue* m_taskQ; //静态成员需要在类外定义
};
TaskQueue* TaskQueue::m_taskQ = nullptr;

int main()
{
	TaskQueue* obj = TaskQueue::getInstance();
}

在调用**getInstance()**函数获取单例对象的时候,如果在单线程情况下是没有什么问题的,如果是多个线程,调用这个函数去访问单例对象就有问题了。假设有三个线程同时执行了getInstance()函数,在这个函数内部每个线程都会new出一个实例对象。此时,这个任务队列类的实例对象不是一个而是3个,很显然这与单例模式的定义是相悖的。

线程安全问题

对于饿汉模式来说是没有线程安全问题的,在这种模式下访问单例对象时,这个对象已经被创建出来了,要解决懒汉模式的线程安全问题,最常用的解决方案就是使用互斥锁,可以将创建单例对象的代码使用互斥锁锁住:

#include
#include
using namespace std;

class TaskQueue
{
public:
	TaskQueue(const TaskQueue& obj) = delete;//禁用拷贝构造
	TaskQueue& operator = (const TaskQueue& obj) = delete;//禁用赋值构造
	static TaskQueue* getInstance()  //获取单例的方法
	{
		m_mutex.lock();
		if (nullptr == m_taskQ)
		{
			cout << "我加了互斥锁" << endl;
			m_taskQ = new TaskQueue;
		}
		m_mutex.unlock();
		return m_taskQ;
	}

private:
	TaskQueue() = default; //无参构造
	static TaskQueue* m_taskQ; //静态成员需要在类外定义
	static mutex m_mutex; //定义为静态的,因为静态函数只能使用静态变量
};
mutex TaskQueue::m_mutex;
TaskQueue* TaskQueue::m_taskQ = nullptr;

int main()
{
	TaskQueue* obj = TaskQueue::getInstance();
}

在上面代码的10~13 行这个代码块被互斥锁锁住了,也就意味着不论有多少个线程,同时执行这个代码块的线程只能是一个(相当于是严重限行了,在重负载情况下,可能导致响应缓慢)。我们可以将代码再优化一下:

#include
#include
using namespace std;

class TaskQueue
{
public:
	TaskQueue(const TaskQueue& obj) = delete;//禁用拷贝构造
	TaskQueue& operator = (const TaskQueue& obj) = delete;//禁用赋值构造
	static TaskQueue* getInstance()  //获取单例的方法
	{
		if (nullptr == m_taskQ)
		{
			m_mutex.lock();
			if (nullptr == m_taskQ)
			{
				cout << "我加了互斥锁" << endl;
				m_taskQ = new TaskQueue;
			}
			m_mutex.unlock();
		}
		return m_taskQ;
	}

private:
	TaskQueue() = default; //无参构造
	static TaskQueue* m_taskQ; //静态成员需要在类外定义
	static mutex m_mutex;
};
mutex TaskQueue::m_mutex;
TaskQueue* TaskQueue::m_taskQ = nullptr;

int main()
{
	TaskQueue* obj = TaskQueue::getInstance();
}

双重检查锁定问题

#include
#include
#include
using namespace std;

class TaskQueue
{
public:
	TaskQueue(const TaskQueue& obj) = delete;//禁用拷贝构造
	TaskQueue& operator = (const TaskQueue& obj) = delete;//禁用赋值构造
	static TaskQueue* getInstance()  //获取单例的方法
	{
		TaskQueue* taskQ = m_taskQ.load(); //取出来单例的值
		if (nullptr == taskQ)
		{
			m_mutex.lock();
			taskQ = m_taskQ.load();
			if (nullptr == taskQ)
			{
				cout << "我加了原子" << endl;
				taskQ = new TaskQueue;
				m_taskQ.store(taskQ); //保存到原子变量中
			}
			m_mutex.unlock();
		}
		return m_taskQ.load();
	}

private:
	TaskQueue() = default; //无参构造
	static atomic<TaskQueue*>m_taskQ; //定义为原子变量
	static mutex m_mutex;
};
mutex TaskQueue::m_mutex;
atomic<TaskQueue*> TaskQueue::m_taskQ;


int main()
{
	TaskQueue* obj = TaskQueue::getInstance();
}

对于m_taskQ = new TaskQueue这行代码来说,我们期望的执行机器指令执行顺序是:
(1)分配用来保存TaskQueue对象的内存;
(2)在分配好的内存中构造一个TaskQueue对象(即初始化内存);
(3)使用m_taskQ指针指向分配的内存。
但是对多线程来说,机器指令可能会被重新排列;即:
(1)分配内存用于保存 TaskQueue 对象。
(2)使用 m_taskQ 指针指向分配的内存。
(3)在分配的内存中构造一个 TaskQueue 对象(初始化内存)。
这样重排序并不影响单线程的执行结果,但是在多线程中就会出问题。如果线程A按照第二种顺序执行机器指令,执行完前两步之后失去CPU时间片被挂起了,此时线程B在第3行处进行指针判断的时候m_taskQ 指针是不为空的,但这个指针指向的内存却没有被初始化,最后线程 B 使用了一个没有被初始化的队列对象就出问题了(出现这种情况是概率问题,需要反复的大量测试问题才可能会出现)。
在C++11中引入了原子变量atomic,通过原子变量可以实现一种更安全的懒汉模式的单例,代码如下:

#include
#include
#include
using namespace std;

class TaskQueue
{
public:
	TaskQueue(const TaskQueue& obj) = delete;//禁用拷贝构造
	TaskQueue& operator = (const TaskQueue& obj) = delete;//禁用赋值构造
	static TaskQueue* getInstance()  //获取单例的方法
	{
		TaskQueue* taskQ = m_taskQ.load(); //取出来单例的值
		if (nullptr == taskQ)
		{
			m_mutex.lock();
			taskQ = m_taskQ.load();
			if (nullptr == taskQ)
			{
				cout << "我加了原子" << endl;
				taskQ = new TaskQueue;
				m_taskQ.store(taskQ); //保存到原子变量中
			}
			m_mutex.unlock();
		}
		return m_taskQ.load();
	}

private:
	TaskQueue() = default; //无参构造
	static atomic<TaskQueue*>m_taskQ; //定义为原子变量
	static mutex m_mutex;
};
mutex TaskQueue::m_mutex;
atomic<TaskQueue*> TaskQueue::m_taskQ;


int main()
{
	TaskQueue* obj = TaskQueue::getInstance();
}

使用局部静态

c++11新特性有如下规定:如果指令逻辑进入一个未被初始化的声明标量,所有并发执行应当等待该变量完成初始化。

#include

using namespace std;

class TaskQueue
{
public:
	TaskQueue(const TaskQueue& obj) = delete;//禁用拷贝构造
	TaskQueue& operator = (const TaskQueue& obj) = delete;//禁用赋值构造
	static TaskQueue* getInstance()  //获取单例的方法
	{
		static TaskQueue m_taskQ; //未被初始化
		return &m_taskQ;
	}
	void print()
	{
		cout << "hello, world!!!" << endl;
	}

private:
	TaskQueue() = default; //无参构造
};



int main()
{
	TaskQueue* obj = TaskQueue::getInstance();
	obj->print();
}

饿汉模式和懒汉模式的区别

懒汉模式的缺点是在创建实例对象的时候有安全问题,但这样可以减少内存的浪费(如果用不到就不去申请内存了)。饿汉模式则相反,在我们不需要这个实例对象的时候,它已经被创建出来,占用了一块内存。对于现在的计算机而言,内存容量都是足够大的,这个缺陷可以被无视。

你可能感兴趣的:(c++设计模式,c++,设计模式,单例模式)