单例模式——C++版本

1.什么是单例模式

在一个项目中,全局范围内,某个类的实例有且仅有一个,通过这个唯一实例向其他模块提供数据的全局访问,这种模式就叫单例模式。

类中多对象的操作函数有如下几个:

  • 构造函数 : 能够创建出一个新对象;
  • 拷贝构造函数 :能够根据一个已经存在的对象拷贝出一个新对象;
  • 赋值操作符重载函数 :用一个对象给另一个对象赋值;

为了使得类全局只有一个实例,我们需要对这些函数做一些处理

  • 构造函数私有化,且在类内部只被调用一次;
    • 由于使用者在类外部不能使用构造函数,所以在类内部创建的这个唯一的对象必须是静态的,这样就可以通过类名来访问了,为了不破坏类的封装,我们都会把这个静态对象的访问权限设置为 private
    • 类中只有它的静态成员函数才能访问其静态成员变量,所以可以给这个单例类提供一个静态函数用于得到这个静态的实例对象
  • 拷贝构造函数 私有化 或者 禁用(private 或者 delete);
  • 赋值操作符重载函数私有化 或者 禁用。(这个操作有没有都没影响)

单例模式的代码模板:

// 定义一个单例模式的类
class Singleton
{
public:// = delete 代表函数禁用, 也可以将其访问权限设置为私有Singleton(const Singleton& rhs) = delete;Singleton& operator=(const Singleton& rhs) = delete;static Singleton* getInstance();
private:Singleton() = default;static Singleton* m_obj;
};

2.单例模式

单例模式可以分为 :懒汉式饿汉式

一、懒汉式

饿汉模式就是在类加载的时候立刻进行实例化,这样就得到了一个唯一的可用对象。

定义:

// 饿汉模式 在调用 get_instance 之前 实例就已经存在了
// 多线程环境下 , 饿汉模式是线程 安全的
class TaskQueue {
public:TaskQueue(const TaskQueue& rhs) = delete;TaskQueue& operator = (const TaskQueue& rhs) = delete;static TaskQueue* get_instance() {return m_task_queue;}void print() {cout << "我是单例对象 TaskQueue 的一个成员函数..." << endl;}private:TaskQueue() = default;static TaskQueue* m_task_queue;
};TaskQueue* TaskQueue::m_task_queue = new TaskQueue;int main()
{TaskQueue* task_queue = TaskQueue::getInstance();task_queue->print();
}

需要注意的是:

  • 在定义这个 TaskQueue 类的时候,这个静态的单例对象 m_task_queue 就已经被创建出来了,当调用 TaskQueue::get_instance() 的时候,对象就已经被实例化了;
  • 类中的静态成员变量需要在类外初始化
  • 饿汉式在多线程环境下是线程安全的

二、懒汉式

懒汉式是在类加载的时候不去创建这个唯一的实例,而是在需要使用的时候再进行实例化

定义:

// 懒汉模式 在调用 get_instance 之前 实例存在 , 第一次调用 get_instance 才会实例化对象
// 多线程环境下, 饿汉模式是线程 不安全的
class TaskQueue {
public:TaskQueue(const TaskQueue& rhs) = delete;TaskQueue& operator = (const TaskQueue& rhs) = delete;static TaskQueue* get_instance() {if (m_task_queue == nullptr) {//在第一次调用 get_instance() 的时候再初始化m_task_queue = new TaskQueue;}return m_task_queue;}void print() {cout << "我是单例对象 TaskQueue 的一个成员函数..." << endl;}private:TaskQueue() = default;static TaskQueue* m_task_queue;
};TaskQueue* TaskQueue::m_task_queue = nullptr;int main()
{TaskQueue* task_queue = TaskQueue::getInstance();task_queue->print();
}

上述代码在单线程环境下是没问题的。但是在多线程环境下,就会出问题,假设多个线程同时调用 get_instance() 函数,并且此时 m_task_queue = nullptr,那么就可能创建出多个实例,这就不符合单例模式的定义。

解决方案一:加锁(效率比较低)

我们可以使用互斥锁 mutex 将创建实例的代码锁住,第一次只有一个线程进来创建对象。

代码:

// 用 双重检测锁定 解决懒汉式多线程环境下线程不安全的问题
class TaskQueue {
public:TaskQueue(const TaskQueue& rhs) = delete;TaskQueue& operator = (const TaskQueue& rhs) = delete;static TaskQueue* get_instance() {m_mutex.lock(); //加锁if (m_task_queue== nullptr){m_task_queue= new TaskQueue;}m_mutex.unlock(); //解锁return m_task_queue;}void print() {cout << "我是单例对象 TaskQueue 的一个成员函数..." << endl;}private:TaskQueue() = default;static TaskQueue* m_task_queue;static mutex m_mutex;
};TaskQueue* TaskQueue::m_task_queue = nullptr;
mutex TaskQueue::m_mutex;

上面代码虽然解决了问题,但是 get_instance() 中的锁住的代码段,每次就只有一个线程来访问,这样效率就非常低

解决方法二:双重检测锁定(存在问题)

双重检测锁定的思路是:在加锁和解锁代码块 之外再加一个 if 判断。这样的话,在第一次调用 get_instance() 的线程仍然会阻塞;第二次调用 get_instance() 的线程,此时 m_task_queue 已经被实例化了,也就是不为 nullptr 了,那么第二次的线程在来到一个 if 判断的时候,就直接退出了,不需要再加锁解锁,这样效率就提升了。

代码:

// 用 双重检测锁定 解决懒汉式多线程环境下线程不安全的问题
class TaskQueue {
public:TaskQueue(const TaskQueue& rhs) = delete;TaskQueue& operator = (const TaskQueue& rhs) = delete;static TaskQueue* get_instance() {//外面再加一层判断if (m_task_queue == nullptr) {m_mutex.lock();if (m_task_queue == nullptr) {m_task_queue = new TaskQueue;}m_mutex.unlock();}return m_task_queue;}void print() {cout << "我是单例对象 TaskQueue 的一个成员函数..." << endl;}private:TaskQueue() = default;static TaskQueue* m_task_queue;static mutex m_mutex;
};TaskQueue* TaskQueue::m_task_queue = nullptr;
mutex TaskQueue::m_mutex;

实际上 双重检测锁定 的代码还是有问题的。

假设此时有两个线程 AB,线程 A 刚好要调用 m_task_queue = new TaskQueue; 这一句代码(假设此时 m_task_queue == nullptr);而线程 B 刚好来到第一个 if 判断。

	static TaskQueue* get_instance() {//线程B 马上进入下面这个 if 判断if (m_task_queue == nullptr) {m_mutex.lock();if (m_task_queue == nullptr) {//线程A 马上调用下面这一句代码m_task_queue = new TaskQueue;}m_mutex.unlock();}return m_task_queue;}

对于 m_task_queue = new TaskQueue; 创建对象的这一句代码,在底层实际上时会被分成三个步骤:

  • 第一步:分配内存用于存储 TaskQueue 对象;
  • 第二步:在分配的内存中构造一个 TaskQueue 对象(初始化内存);
  • 第三步:指针 m_task_queue 指向分配的内存;

由于编译器底层对我们的代码进行优化,就会将这些指令进行重排序,也就是打乱了它本来的步骤。

比如说将上述的步骤重排序之后,变成下面的:

  • 第一步:分配内存用于存储 TaskQueue 对象;
  • 第二步:指针 m_task_queue 指向分配的内存;
  • 第三步:在分配的内存中构造一个 TaskQueue 对象(初始化内存);

即 第二步 和 第三步 颠倒了顺序。

指令重排序在单线程下没有问题,在多线程下就有可能出现问题。

假设线程 A 此时刚好把前两步执行完了,m_task_queue 此时已经指向一块内存了,不过对这块内存进行操作是非法操作,因为创建对象还没有完成;线程 B 此时正好,进入第一个 if 判断,此时 m_task_queue 不为 nullptr,就直接退出,返回了没有构造完全的对象 m_task_queue

如果线程 B 对这个对象进行操作,就会出问题。

解决方法三:双重检测锁定 + 原子变量 (效率更低)

C++ 11 引入了 原子变量 atomic 可以解决 双重检测锁定 的问题。

代码:

// 用 原子变量 解决双重检测 的问题
class TaskQueue {
public:TaskQueue(const TaskQueue& rhs) = delete;TaskQueue& operator = (const TaskQueue& rhs) = delete;static TaskQueue* get_instance() {TaskQueue* task_queue = m_task_queue.load();if (task_queue == nullptr) {m_mutex.lock();task_queue = m_task_queue.load();if (task_queue == nullptr) {task_queue = new TaskQueue;m_task_queue.store(task_queue);}m_mutex.unlock();}return task_queue;}void print() {cout << "我是单例对象 TaskQueue 的一个成员函数..." << endl;}private:TaskQueue() = default;//static TaskQueue* m_task_queue;static atomic<TaskQueue*> m_task_queue;static mutex m_mutex;
};//TaskQueue* TaskQueue::m_task_queue = nullptr;
atomic<TaskQueue*> TaskQueue::m_task_queue;
mutex TaskQueue::m_mutex;

上面代码中使用原子变量 atomicstore() 函数来存储单例对象,使用 load() 函数来加载单例对象。

在原子变量中这两个函数在处理指令的时候默认的原子顺序是 memory_order_seq_cst即顺序原子操作 - sequentially consistent),这样也就避免了之前的指令重排的问题,使用顺序约束原子操作库,整个函数执行都将保证顺序执行,并且不会出现数据竞态(data races),缺点就是使用这种方法实现的懒汉模式的单例执行效率更低一些

解决方法四:静态局部变量(推荐)

在 C++ 11 直接使用 静态局部变量 在多线程环境下是不会出现问题的。

代码:

class TaskQueue
{
public:// = delete 代表函数禁用, 也可以将其访问权限设置为私有TaskQueue(const TaskQueue& rhs) = delete;TaskQueue& operator=(const TaskQueue& rhs) = delete;static TaskQueue* getInstance(){static TaskQueue task_queue;return &task_queue;}void print(){cout << "我是单例对象 TaskQueue 的一个成员函数..." << endl;}private:TaskQueue() = default;
};int main()
{TaskQueue* queue = TaskQueue::getInstance();queue->print();return 0;
}

之所以上面代码是线程安全的 ,是因为 C++ 11 规定了,并且这个操作是在编译时由编译器保证的:

如果指令逻辑进入一个未被初始化的声明变量,所有并发执行应当等待该变量完成初始化。

三、总结

  • 懒汉模式的缺点是在创建实例对象的时候有安全问题,但这样可以减少内存的浪费(如果用不到就不去申请内存了)。
  • 饿汉模式则相反,在我们不需要这个实例对象的时候,它已经被创建出来,占用了一块内存。

四、练习

实现一个 任务队列。生产者线程生产任务加入任务队列;消费者线程取出任务队列的任务执行。

类成员:

  • 存储任务的容器,我们直接使用 STL 中的容器 queue
  • 互斥锁(mutex),在多线程访问的情况下,用于保护共享数据;

成员函数:

  • 判断任务队列是否为空;
  • 往任务队列中添加一个任务;
  • 往任务队列总删除一个任务;
  • 从任务队列中取出一个任务

为了简单起见,我们用一个 int 数,表示一个任务。

代码:

#if 1
// 用局部静态变量饿汉式单例 实现任务队列class TaskQueue {
public:TaskQueue(const TaskQueue& rhs) = delete;TaskQueue& operator = (const TaskQueue& rhs) = delete;static TaskQueue* get_instance() {static TaskQueue task_queue;return &task_queue;}//判断任务队列是否为空bool is_empty() {lock_guard<mutex> locker(m_mutex);return q.empty();}//删除任务bool delete_task() {lock_guard<mutex> locker(m_mutex);if (q.empty()) return false;q.pop();return true;}//取出任务 (不删除任务)int take_task() {lock_guard<mutex> locker(m_mutex);if (q.empty()) return -1;return q.front();}//添加任务void add_task(int task) {lock_guard<mutex> locker(m_mutex);q.push(task);}private:TaskQueue() = default;queue<int> q;mutex m_mutex;
};#endifint main() {TaskQueue* task_queue = TaskQueue::get_instance();thread t1([=]() {//生产者 t1 给任务队列添加10个任务for (int i = 0; i < 10; i++) {int task = i + 100;task_queue->add_task(task);cout << "producer thread produce a task : " << task << " , thread id is " << this_thread::get_id() << endl;this_thread::sleep_for(chrono::milliseconds(500));}});thread t2([=](){//让生产者线程先执行 保证先有任务this_thread::sleep_for(chrono::milliseconds(500));while (!task_queue->is_empty()) {int task = task_queue->take_task();task_queue->delete_task();cout << "consumer thread consume a task : " << task << " , thread id is " << this_thread::get_id() << endl;this_thread::sleep_for(chrono::milliseconds(1000));}});t1.join();t2.join();return 0;
}

3.参考

本篇博客是对于 :单例模式 的整理。

  • C++ 单例模式总结(5种单例实现方法)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/69914.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

加强版python连接飞书通知——本地电脑PC端通过网页链接打开本地已安装软件(调用注册表形式,以漏洞扫描工具AppScan为例)

前言 如果你想要通过超链接来打开本地应用,那么你首先你需要将你的应用添入windows注册表中(这样网页就可以通过指定代号来调用程序),由于安全性的原因所以网页无法直接通过输入绝对路径来调用本地文件。 一、通过创建reg文件自动配置注册表 创建文本文档,使用记事本打开…

STM32使用FAT文件系统-常见概念、代码走读

fat文件系统的所有外部接口都在ff.h中 格式化 挂载 使用文件系统的第一步&#xff0c;就是挂载。 函数原型&#xff1a;FRESULT f_mount (FATFS* fs, const TCHAR* path, BYTE opt); /* Mount/Unmount a logical drive */ static FATFS fs; FRESULT fres f_mo…

【爬虫】7.4. 字体反爬案例分析与爬取实战

字体反爬案例分析与爬取实战 文章目录 字体反爬案例分析与爬取实战1. 案例介绍2. 案例分析3. 爬取 本节来分析一个反爬案例&#xff0c;该案例将真实的数据隐藏到字体文件里&#xff0c;即使我们获取了页面源代码&#xff0c;也无法直接提取数据的真实值。 1. 案例介绍 案例网…

第2节-PhotoShop基础课程-基础操作

文章目录 前言一、详细操作1. 新建 Ctrl N2.打开 Ctrl o1.拖入同一个文件2.打开另外一个文件 3. 首选项 提高软件运行效率 Ctrl K1. 选择ps界面颜色2.文件处理3. 内存以及回操4.暂存位置2. 另存为 Ctrl Shift S3.导出为 Ctrl Alt Shift w 5. 关闭1. 关闭Ctrl W 6. 选择7…

存储过程报Illegal mix of collations错误的解决方法

CREATE PROCEDURE maxAgeStudent(IN _gender CHAR) BEGINDECLARE maxage INT DEFAULT 0;SELECT max(age) INTO maxage FROM student where gender _gender;SELECT * from student WHERE age maxage and gender _gender; END; 在调用的时候 call maxAgeStudent(1) 产生了报…

SpringCloud入门实战(十五)分布式事务框架Seata简介

&#x1f4dd; 学技术、更要掌握学习的方法&#xff0c;一起学习&#xff0c;让进步发生 &#x1f469;&#x1f3fb; 作者&#xff1a;一只IT攻城狮 &#xff0c;关注我&#xff0c;不迷路 。 &#x1f490;学习建议&#xff1a;1、养成习惯&#xff0c;学习java的任何一个技术…

利用梯度下降法求最小值及动画展示

视频讲解链接:3.1梯度下降法求最小值_哔哩哔哩_bilibili# 利用梯度下降法求最小值 import matplotlib.pyplot as plt from matplotlib.animation import FuncAnimation import numpy as np plt.rcParams[font.sans-serif] = [SimHei] # 用来正常显示中文标签 plt.rcParams[ax…

qemu/kvm学习笔记

qemu/kvm架构 cpu虚拟化的示例 Reference: kvmtest.c [LWN.net] 主要步骤&#xff1a; QEMU通过/dev/kvm设备文件发起KVM_CREATE_VM ioctl&#xff0c;请求KVM创建一个虚拟机。KVM创建虚拟机相应的结构体&#xff0c;并为QEMU返回一个虚拟机文件描述符QEMU通过虚拟机文件描述…

学习设计模式之代理模式,但是宝可梦

前言 作者在准备秋招中&#xff0c;学习设计模式&#xff0c;做点小笔记&#xff0c;用宝可梦为场景举例&#xff0c;有错误欢迎指出。 代码同步更新到 github ,要是点个Star您就是我的神 目录 前言代理模式1.情景模拟1.1静态代理优点局限 1.2 动态代理 2.应用3.局限4.解决方…

读懂AUTOSAR,之CAN Driver L-PDU发送和“重入问题”

1. L-PDU发送 L-PDU传输时,Can模块将L-PDU内容ID和数据长度转换为硬件特定格式(如果需要),并触发传输。 [SWS_Can_00059] CAN到内存的数据映射定义为首先发送的CAN数据字节为数组元素0,最后发送的CAN数据字节为数组元素7或63(在CAN FD的情况下)。(SRS_SPAL_12063)[S…

CSDN每日一练 |『生命进化书』『订班服』『c++难题-大数加法』2023-09-06

CSDN每日一练 |『生命进化书』『订班服』『c++难题-大数加法』2023-09-06 一、题目名称:生命进化书二、题目名称:订班服三、题目名称:c++难题-大数加法一、题目名称:生命进化书 时间限制:1000ms内存限制:256M 题目描述: 小A有一本生命进化书,以一个树形结构记载了所有生…

leetcode分类刷题:易混题辨析一、209. 长度最小的子数组 vs 560. 和为K的子数组

1、刷题慢慢积累起来以后&#xff0c;遇到相似的题目时&#xff0c;会出现算法思路混淆了 2、这两道题都是对连续子数组加和进行考察&#xff0c;细节区别在于数组元素在209. 长度最小的子数组为正整数&#xff08;窗口增加元素递增&#xff0c;减少元素递减&#xff09;&#…

linux 进程管理命令

进程管理命令 查看进程命令 ps命令 显示系统上运行的进程列表 # 查看系统中所有正在运行的系统ps aux# 获取占用内存资源最多的10个进程&#xff0c;可以使用如下命令组合&#xff1a;ps aux|head -1;ps aux|grep -v PID|sort -rn -k 4|head# 获取占用CPU资源最多的10个进程&am…

PXE批量装机

目录 前言 一、交互式 &#xff08;一&#xff09;、搭建环境 &#xff08;二&#xff09;、配置dhcp服务 &#xff08;三&#xff09;、FTP服务 &#xff08;四&#xff09;、配置TFTP服务 &#xff08;五&#xff09;、准备pxelinx.0文件、引导文件、内核文件 &#…

C#开发的OpenRA游戏之信标按钮

前面已经分析了两个按钮:变卖和维修,接着下来就是分析信标按钮,这个按钮使用是比较少,但是对于多人游戏时,使用这个信号就很方便同盟军过来查看和帮助了,相当于一个朋友之间共同查看的地址。当你经过同盟标记的标志时,就会听到beacon detected,检测到信标,这就是你的盟…

如何系统地学习 JavaScript?

前言 在学习JavaScript前需要先将Html和Css的相关知识点弄清楚&#xff0c;Js的很多操作是要结合Html和Css&#xff0c;下面我总结了Html、Css和Js的相关学习知识点供参考&#xff0c;希望对你有所帮助喔~ Html 文档学习 【HTML 】w3school教程 :https://www.w3school.com.…

【SpringBoot入门】详解@Autowired的使用

【SpringBoot入门】详解Autowired的使用 在构造函数中使用Autowired的注解的简单实例除了构造函数&#xff0c;Autowired注解还可以用在哪些地方&#xff1f;方法上&#xff1a;字段上&#xff1a;Setter方法上&#xff1a;构造函数参数上&#xff1a; 在构造函数中使用Autowir…

Java分别用BIO、NIO实现简单的客户端服务器通信

分别用BIO、NIO实现客户端服务器通信 BIONIONIO演示&#xff08;无Selector&#xff09;NIO演示&#xff08;Selector&#xff09; 前言&#xff1a; Java I/O模型发展以及Netty网络模型的设计思想 BIO Java BIO是Java平台上的BIO&#xff08;Blocking I/O&#xff09;模型&a…

树状数组,线段树,容斥,P3801 红色的幻想乡

P3801 红色的幻想乡 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn) 题目背景 蕾米莉亚的红雾异变失败后&#xff0c;很不甘心。 题目描述 经过上次失败后&#xff0c;蕾米莉亚决定再次发动红雾异变&#xff0c;但为了防止被灵梦退治&#xff0c;她决定将红雾以奇怪的阵势释…

监控基本概念

监控&#xff1a;这个词在不同的上下文中有不同的含义&#xff0c;在讲到监控MySQL或者监控Redis时&#xff0c;这里只涉及数据采集和可视化&#xff0c;不涉及告警引擎和事件处理。要是监控系统的话&#xff0c;不但包括数据采集和可视化&#xff0c;而且也包括告警和事件发送…