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

单例模式是什么

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

为什么要使用单例模式

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

在这里插入图片描述

饿汉模式

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

#include<iostream>
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<iostream>
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<iostream>
#include<mutex>
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<iostream>
#include<mutex>
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<iostream>
#include<mutex>
#include<atomic>
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<iostream>
#include<mutex>
#include<atomic>
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<iostream>

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++中,当声明一个静态成员变量时,如果没有显式地初始化它,它将被默认初始化为零值或空值,具体取决于变量的类型。
对于类对象类型的静态成员变量,会自动调用默认构造函数进行初始化。在这种情况下,m_taskQ 将会被初始化为TaskQueue类的默认构造函数创建的对象

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

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

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

<( ̄︶ ̄)Okay.

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值