创建型模式--1.单例模式【巴基速递】

1. 巴基的订单

在海贼世界中,巴基速递是巴基依靠手下强大的越狱犯兵力,组建的集团海贼派遣公司,它的主要业务是向世界有需要的地方输送雇佣兵(其实是不干好事儿)。

在这里插入图片描述

自从从特拉法尔加罗路飞同盟击败了堂吉诃德家族 ,战争的市场对雇佣兵的依赖越来越大。订单便源源不断的来了。此时我们来分析一个问题:巴基是怎么接单并且派单的呢?

简单来说,巴基肯定是有一个账本用于记录下单者信息,下单者的需求以及下单的时间,然后根据下单的先后顺序选择合适的人手进行派单。从程序猿的视角可以这样认为,这个账本其实就相当于一个任务队列:

  • 有一定的容量,可以存储任务
  • 按照下单的先后顺序存储并处理任务 – 典型的队列特性:先进先出

对于巴基来说把所有的订单全部记录到一个账本上就够了,如果将其平移到项目中,也就意味着应用程序在运行过程中存储任务的任务队列一个足矣,弄太多反而冗余,不太好处理了。

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

在这里插入图片描述

2. 独生子女

如果使用单例模式,首先要保证这个类的实例有且仅有一个,也就是说这个对象是独生子女,如果我们实施计划生育只生一个孩子,不需要也不能给再他增加兄弟姐妹。因此,就必须采取一系列的防护措施。对于类来说以上描述同样适用。涉及一个类多对象操作的函数有以下几个:

  • 构造函数:创建一个新的对象
  • 拷贝构造函数:根据已有对象拷贝出一个新的对象
  • 拷贝赋值操作符重载函数:两个对象之间的赋值

为了把一个类可以实例化多个对象的路堵死,可以做如下处理:

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

  2. 拷贝赋值操作符重载函数私有化或者禁用(从单例的语义上讲这个函数已经毫无意义,所以在类中不再提供这样一个函数,故将它也一并处理一下。

由于单例模式就是给类创建一个唯一的实例对象,所以它的UML类图是很简单的:
在这里插入图片描述

因此,定义一个单例模式的类的示例代码如下:

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

在实现一个单例模式的类的时候,有两种处理模式:

  • 饿汉模式
  • 懒汉模式

3. 饿汉模式

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

// 饿汉模式
class TaskQueue
{
public:// = delete 代表函数禁用, 也可以将其访问权限设置为私有TaskQueue(const TaskQueue& obj) = delete;TaskQueue& operator=(const TaskQueue& obj) = delete;static TaskQueue* getInstance(){return m_taskQ;}
private:TaskQueue() = default;static TaskQueue* m_taskQ;
};
// 静态成员初始化放到类外部处理
TaskQueue* TaskQueue::m_taskQ = new TaskQueue;int main()
{TaskQueue* obj = TaskQueue::getInstance();
}

第17行,定义这个单例类的时候,就把这个静态的单例对象创建出来了。当使用者通过getInstance()获取这个单例对象的时候,它已经被准备好了。

注意事项:类的静态成员变量在使用之前必须在类的外部进行初始化才能使用。

4. 懒汉模式

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

4.1 懒汉模式类的定义

// 懒汉模式
class TaskQueue
{
public:// = delete 代表函数禁用, 也可以将其访问权限设置为私有TaskQueue(const TaskQueue& obj) = delete;TaskQueue& operator=(const TaskQueue& obj) = delete;static TaskQueue* getInstance(){if(m_taskQ == nullptr){m_taskQ = new TaskQueue;}return m_taskQ;}
private:TaskQueue() = default;static TaskQueue* m_taskQ;
};
TaskQueue* TaskQueue::m_taskQ = nullptr;

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

4.2 线程安全问题

双重检查锁定

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

class TaskQueue
{
public:// = delete 代表函数禁用, 也可以将其访问权限设置为私有TaskQueue(const TaskQueue& obj) = delete;TaskQueue& operator=(const TaskQueue& obj) = delete;static TaskQueue* getInstance(){m_mutex.lock();if (m_taskQ == nullptr){m_taskQ = new TaskQueue;}m_mutex.unlock();return m_taskQ;}
private:TaskQueue() = default;static TaskQueue* m_taskQ;static mutex m_mutex;
};
TaskQueue* TaskQueue::m_taskQ = nullptr;
mutex TaskQueue::m_mutex;

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

class TaskQueue
{
public:// = delete 代表函数禁用, 也可以将其访问权限设置为私有TaskQueue(const TaskQueue& obj) = delete;TaskQueue& operator=(const TaskQueue& obj) = delete;static TaskQueue* getInstance(){if (m_taskQ == nullptr){m_mutex.lock();if (m_taskQ == nullptr){m_taskQ = new TaskQueue;}m_mutex.unlock();}return m_taskQ;}
private:TaskQueue() = default;static TaskQueue* m_taskQ;static mutex m_mutex;
};
TaskQueue* TaskQueue::m_taskQ = nullptr;
mutex TaskQueue::m_mutex;

改进的思路就是在加锁、解锁的代码块外层有添加了一个if判断(第9行),这样当任务队列的实例被创建出来之后,访问这个对象的线程就不会再执行加锁和解锁操作了(只要有了单例类的实例对象,限行就解除了),对于第一次创建单例对象的时候线程之间还是具有竞争关系,被互斥锁阻塞。上面这种通过两个嵌套的 if 来判断单例对象是否为空的操作就叫做双重检查锁定

双重检查锁定的问题

假设有两个线程A、B,当线程A 执行到第 8 行时在线程A中 TaskQueue 实例对象 被创建,并赋值给 m_taskQ

static TaskQueue* getInstance()
{if (m_taskQ == nullptr){m_mutex.lock();if (m_taskQ == nullptr){m_taskQ = new TaskQueue;}m_mutex.unlock();}return m_taskQ;
}

但是实际上 m_taskQ = new TaskQueue; 在执行过程中对应的机器指令可能会被重新排序。正常过程如下:

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

但是被重新排序以后执行顺序可能会变成这样:

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

这样重排序并不影响单线程的执行结果,但是在多线程中就会出问题。如果线程A按照第二种顺序执行机器指令,执行完前两步之后失去CPU时间片被挂起了,此时线程B在第3行处进行指针判断的时候m_taskQ 指针是不为空的,但这个指针指向的内存却没有被初始化,最后线程 B 使用了一个没有被初始化的队列对象就出问题了(出现这种情况是概率问题,需要反复的大量测试问题才可能会出现)。

在C++11中引入了原子变量atomic,通过原子变量可以实现一种更安全的懒汉模式的单例,代码如下:

class TaskQueue
{
public:// = delete 代表函数禁用, 也可以将其访问权限设置为私有TaskQueue(const TaskQueue& obj) = delete;TaskQueue& operator=(const TaskQueue& obj) = delete;static TaskQueue* getInstance(){TaskQueue* queue = m_taskQ.load();  if (queue == nullptr){// m_mutex.lock();  // 加锁: 方式1lock_guard<mutex> locker(m_mutex);  // 加锁: 方式2queue = m_taskQ.load();if (queue == nullptr){queue = new TaskQueue;m_taskQ.store(queue);}// m_mutex.unlock();}return queue;}void print(){cout << "hello, world!!!" << endl;}
private:TaskQueue() = default;static atomic<TaskQueue*> m_taskQ;static mutex m_mutex;
};
atomic<TaskQueue*> TaskQueue::m_taskQ;
mutex TaskQueue::m_mutex;int main()
{TaskQueue* queue = TaskQueue::getInstance();queue->print();return 0;
}

上面代码中使用原子变量atomicstore() 方法来存储单例对象,使用load() 方法来加载单例对象。在原子变量中这两个函数在处理指令的时候默认的原子顺序是memory_order_seq_cst(顺序原子操作 - sequentially consistent),使用顺序约束原子操作库,整个函数执行都将保证顺序执行,并且不会出现数据竞态(data races),不足之处就是使用这种方法实现的懒汉模式的单例执行效率更低一些

静态局部对象

在实现懒汉模式的单例的时候,相较于双重检查锁定模式有一种更简单的实现方法并且不会出现线程安全问题,那就是使用静态局部局部对象,对应的代码实现如下:

class TaskQueue
{
public:// = delete 代表函数禁用, 也可以将其访问权限设置为私有TaskQueue(const TaskQueue& obj) = delete;TaskQueue& operator=(const TaskQueue& obj) = delete;static TaskQueue* getInstance(){static TaskQueue taskQ;return &taskQ;}void print(){cout << "hello, world!!!" << endl;}private:TaskQueue() = default;
};int main()
{TaskQueue* queue = TaskQueue::getInstance();queue->print();return 0;
}

在程序的第 9、10 行定义了一个静态局部队列对象,并且将这个对象作为了唯一的单例实例。使用这种方式之所以是线程安全的,是因为在C++11标准中有如下规定,并且这个操作是在编译时由编译器保证的:

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

最后总结一下懒汉模式和饿汉模式的区别:

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

5. 替巴基写一个任务队列

作为程序猿的我们,如果想给巴基的账本升级成一个应用程序,首要任务就是设计一个单例模式的任务队列,那么就需要赋予这个类一些属性和方法:

  1. 属性:
    • 存储任务的容器,这个容器可以选择使用STL中的队列(queue)
    • 互斥锁,多线程访问的时候用于保护任务队列中的数据
  2. 方法:主要是对任务队列中的任务进行操作
    • 任务队列中任务是否为空
    • 往任务队列中添加一个任务
    • 从任务队列中取出一个任务
    • 从任务队列中删除一个任务

根据分析,就可以把这个饿汉模式的任务队列的单例类定义出来了:

#include <iostream>
#include <queue>
#include <mutex>
#include <thread>
using namespace std;class TaskQueue
{
public:// = delete 代表函数禁用, 也可以将其访问权限设置为私有TaskQueue(const TaskQueue& obj) = delete;TaskQueue& operator=(const TaskQueue& obj) = delete;static TaskQueue* getInstance(){return &m_obj;}// 任务队列是否为空bool isEmpty(){lock_guard<mutex> locker(m_mutex);bool flag = m_taskQ.empty();return flag;}// 添加任务void addTask(int data){lock_guard<mutex> locker(m_mutex);m_taskQ.push(data);}// 取出一个任务int takeTask(){lock_guard<mutex> locker(m_mutex);if (!m_taskQ.empty()){return m_taskQ.front();}return -1;}// 删除一个任务bool popTask(){lock_guard<mutex> locker(m_mutex);if (!m_taskQ.empty()){m_taskQ.pop();return true;}return false;}
private:TaskQueue() = default;static TaskQueue m_obj;queue<int> m_taskQ;mutex m_mutex;
};
TaskQueue TaskQueue::m_obj;int main()
{thread t1([]() {TaskQueue* taskQ = TaskQueue::getInstance();for (int i = 0; i < 100; ++i){taskQ->addTask(i + 100);cout << "+++push task: " << i + 100 << ", threadID: " << this_thread::get_id() << endl;this_thread::sleep_for(chrono::milliseconds(500));}});thread t2([]() {TaskQueue* taskQ = TaskQueue::getInstance();this_thread::sleep_for(chrono::milliseconds(100));while (!taskQ->isEmpty()){int data = taskQ->takeTask();cout << "---take task: " << data << ", threadID: " << this_thread::get_id() << endl;taskQ->popTask();this_thread::sleep_for(chrono::seconds(1));}});t1.join();t2.join();
}

在上面的程序中有以下几点需要说明一下:

  • 正常情况下,任务队列中的任务应该是一个函数指针(这个指针指向的函数中有需要执行的任务动作),此处进行了简化,用一个整形数代替了任务队列中的任务。
  • 任务队列中的互斥锁保护的是单例对象的中的数据也就是任务队列中的数据,上面所说的线程安全指的是在创建单例对象的时候要保证这个对象只被创建一次,和此处完全是两码事儿,需要区别看待。

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

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

相关文章

【ArcGIS学习笔记】ArcMap打开就卡在文档加载界面好久不动,打开很慢,要好长时间

Arcmap之前用得好好&#xff0c;后来打开就卡在文档加载界面&#xff0c;然后界面就关闭了&#xff1f; - 知乎针对这一情况&#xff0c;主要有下面五种解决方法。其中&#xff0c;对于大部分用户而言&#xff0c;前两种方法大概率是可以解决问题的&#xff1b;…https://www.z…

RocketMQ的docker安装和SpringBoot的集成

1.Docker安装 1.1创建docker-compose.yml文件 version: 3.5 services:rmqnamesrv:image: foxiswho/rocketmq:servercontainer_name: rmqnamesrvports:- 9876:9876networks:rmq:aliases:- rmqnamesrvrmqbroker:image: foxiswho/rocketmq:brokercontainer_name: rmqbrokerports…

C++模板初阶(个人笔记)

模板初阶 1.泛型编程2.函数模板2.1函数模板的实例化2.2模板参数的匹配规则 3.类模板3.1类模板的实例化 1.泛型编程 泛型编程&#xff1a;编写与类型无关的通用代码&#xff0c;是代码复用的一种手段。模板是泛型编程的基础。 //函数重载 //交换函数的逻辑是一致的&#xff0c…

SpringBoot学习笔记四

SpringBoot学习笔记四-监听机制 1. SpringBoot监听器1.1 无需配置1.1.1 CommandLineRunner使用1.1.2 ApplicationRunner的使用1.1.3 CommandLineRunner与ApplicationRunner的区别 1.2 需要创建META-INF文件&#xff0c;并在其中创建spring.factories&#xff0c;配置相关的信息…

【CSDN创作优化2】内嵌图片 `<img>` 标签`height`和`width`属性

【CSDN创作优化2】内嵌图片 标签height和width属性 写在最前面<img> 标签简介控制图像尺寸&#xff1a;height和width属性实例为什么要指定height和width注意事项 使用百分比进行响应式设计小结 &#x1f308;你好呀&#xff01;我是 是Yu欸 &#x1f30c; 2024每日百字…

【赛题】2024年“认证杯”数模网络挑战赛赛题发布

2024年"认证杯"数学建模网络挑战赛——正式开赛&#xff01;&#xff01;&#xff01; 赛题已发布&#xff0c;后续无偿分享各题的解题思路、参考文献、完整论文可运行代码&#xff0c;帮助大家最快时间&#xff0c;选择最适合是自己的赛题。祝大家都能取得一个好成…

[dvwa] file upload

file upload 0x01 low 直接上传.php 内容写<? eval($_POST[jj]);?> 用antsword连 路径跳两层 0x02 medium 添加了两种验证&#xff0c;格式为图片&#xff0c;大小限制小于1000 上传 POST /learndvwa/vulnerabilities/upload/ HTTP/1.1 Host: dvt.dv Content-Le…

WORD——效率提升10倍的18个神操作

1、万能F4键 在Word中F4 键的功能是重复上一步操作&#xff0c;也就说上一步你做了什么操作&#xff0c;只要按F4键&#xff0c;它就会自动帮你重来一次。比如&#xff0c;合并单元格&#xff0c;你再也不用反复去点合并按钮&#xff0c;只要合并第一个单元格后&#xff0c;剩…

四种算法(麻雀搜索算法SSA、螳螂搜索算法MSA、红尾鹰算法RTH、霸王龙优化算法TROA)求解机器人路径规划(提供MATLAB代码)

一、机器人路径规划介绍 移动机器人&#xff08;Mobile robot&#xff0c;MR&#xff09;的路径规划是 移动机器人研究的重要分支之&#xff0c;是对其进行控制的基础。根据环境信息的已知程度不同&#xff0c;路径规划分为基于环境信息已知的全局路径规划和基于环境信息未知或…

第06章 网络传输介质

6.1 本章目标 了解双绞线分类和特性了解同轴电缆分类和特性了解光纤分类和特性了解无线传输介质分类和特性 6.2 传输介质分类 现在社会还是以有线介质为主 计算机通信 - 有线通信 - 无线通信有线通信传输介质 - 双绞线 - 同轴电缆 - 光导纤维无线通信 - 卫星 - 微波 - 红外…

【SpringBoot】-- 项目实现微信公众号扫码登录

目录 一、业务需求 二、内网穿透 三、服务器配置 ​编辑 四、依赖引入 pom.xml 五、验证服务器有效性 代码 controller类 SHA1工具类 六、用户订阅后自动回复消息 代码 controller类 MessageUtil工具类 七、用户发送文本消息后回复消息 代码 controller类 八、…

基于SpringBoot+Vue的工厂生产设备维护管理系统(源码+文档+部署+讲解)

一.系统概述 随着社会的发展&#xff0c;系统的管理形势越来越严峻。越来越多的用户利用互联网获得信息&#xff0c;但各种信息鱼龙混杂&#xff0c;信息真假难以辨别。为了方便用户更好的获得工厂生产设备维护信息&#xff0c;因此&#xff0c;设计一种安全高效的工厂生产设备…

示波器接上机器板子信号就正常工作,拿下来就机器不正常工作

系列文章目录 1.元件基础 2.电路设计 3.PCB设计 4.元件焊接 5.板子调试 6.程序设计 7.算法学习 8.编写exe 9.检测标准 10.项目举例 11.职业规划 送给大学毕业后找不到奋斗方向的你&#xff08;每周不定时更新&#xff09; 【牛客网】构建从学习到职业的良性生态圈 中国计算…

Windows系统安装WinSCP结合内网穿透实现公网远程SSH本地服务器

List item 文章目录 1. 简介2. 软件下载安装&#xff1a;3. SSH链接服务器4. WinSCP使用公网TCP地址链接本地服务器5. WinSCP使用固定公网TCP地址访问服务器 1. 简介 ​ Winscp是一个支持SSH(Secure SHell)的可视化SCP(Secure Copy)文件传输软件&#xff0c;它的主要功能是在本…

一文读懂RISC-V与ARM

RISC-V和ARM是近年来备受关注的两种处理器架构。RISC-V是一种基于精简指令集计算(RISC)原理的开源指令集架构(ISA)&#xff0c;而ARM是一种专有ISA&#xff0c;由于其长期存在于嵌入式系统和移动设备中&#xff0c;已成为嵌入式系统和移动设备的主导选择。市场以及多年积累的信…

前端开发攻略---利用Flexbox和Margin实现智能布局:如何巧妙分配剩余空间,让你的网页设计更上一层楼?

1、演示 2、flex布局 Flex布局是一种用于Web开发的弹性盒子布局模型&#xff0c;它可以让容器内的子元素在空间分配、对齐和排列方面具有更大的灵活性。以下是Flex布局的基本用法&#xff1a; 容器属性&#xff1a; display: flex;&#xff1a;将容器指定为Flex布局。flex-dire…

Realme GT Neo6 SE ROOT 解锁BL教程

Realme GTNeo6 SE 解锁ROOT教程 前言&#xff1a; 本文解锁BL教程以及深度测试APP来自Realme官方社区。相关操作流程已进行简化&#xff0c;工具由本人制作并提供&#xff0c;降低上手难度&#xff0c;傻瓜式操作&#xff08;工具长期免费更新&#xff09;。 正文&#xff1a…

Python docx:在Python中创建和操作Word文档

使用docx库&#xff0c;可以执行各种任务 创建新文档&#xff1a;可以使用库从头开始或基于模板生成新的Word文档。这对于自动生成报告、信函和其他类型的文档非常有用。修改现有文档&#xff1a;可以打开现有的Word文档&#xff0c;并使用库修改其内容、格式、样式等。这对于…

2024高交会-2024深圳高新技术展-高新技术成果交易会

2024高交会-2024深圳高新技术展-2024高新技术成果展-中国高校技术交易会-第26届高交会-深圳高交会-深圳高科技展-深圳新科技展-深圳高新技术成果展 第二十六届中国国际高新技术成果交易会&#xff08;简称高交会&#xff09; 时间&#xff1a;2024年11月15日-19日 地址&#…

python+Flask+django企业仓库进销存管理信息系统35wiz

Flask提供了更大的灵活性和简单性&#xff0c;适合小型项目和微服务。Django则提供了更多的内置功能&#xff0c;适合大型项目。Flask让开发者更多的控制其组件&#xff0c;而Django则遵循开箱即用的原则 本课题使用Python语言进行开发。代码层面的操作主要在PyCharm中进行&am…