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

news2024/11/29 2:52:17

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();  // 加锁: 方式1
            lock_guard<mutex> locker(m_mutex);  // 加锁: 方式2
            queue = 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.coloradmin.cn/o/1576701.html

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

相关文章

系统监测工具-tcpdump的使用

一个简单的tcpdump抓包过程。主要抓包观察三次握手&#xff0c;四次挥手的数据包 有两个程序&#xff1a;客户端和服务器两个程序 服务器端的ip地址使用的是回环地址127.0.0.1 端口号使用的是6000 tcpdump -i 指定用哪个网卡等&#xff0c;dstip地址端口指定抓取目的地址…

ctfshow web入门 文件包含 web151--web161

web151 打算用bp改文件形式(可能没操作好)我重新试了一下抓不到 文件上传不成功 改网页前端 鼠标右键&#xff08;检查&#xff09;&#xff0c;把png改为php访问&#xff0c;执行命令 我上传的马是<?php eval($_POST[a]);?> 查看 web152 上传马 把Content-Type改为…

基于大模型的态势认知智能体

源自&#xff1a;指挥控制与仿真 作者&#xff1a;孙怡峰, 廖树范, 吴疆 李福林 “人工智能技术与咨询” 发布 摘要 针对战场态势信息众多、变化趋势认知困难的问题,提出基于大模型的态势认知智能体框架和智能态势认知推演方法。从认知概念出发,结合智能体的抽象性、具…

基于单片机收音机调幅系统设计仿真源码

**单片机设计介绍&#xff0c;基于单片机收音机调幅系统设计仿真源码 文章目录 一 概要二、功能设计设计思路 三、 软件设计原理图 五、 程序六、 文章目录 一 概要 基于单片机收音机调幅系统设计的仿真源码&#xff0c;主要实现了通过单片机控制调幅收音机的核心功能。以下是…

如何关闭WordPress的自动更新功能

Wordpress为什么自动更新 WordPress自动更新是为了提供更好的安全性和稳定性。 安全性&#xff1a;WordPress是一个广泛使用的内容管理系统&#xff0c;因此成为恶意攻击的目标。WordPress的自动更新功能确保你的网站及时获得最新的安全补丁和修复程序&#xff0c;以保护你的网…

ctfshow web入门 命令运行 web39---web52

ctfshow web入门 命令执行 昨天看了一下我的博客真的很恼火&#xff0c;不好看&#xff0c;还是用md来写吧 web39 查看源代码 看到include了&#xff0c;还是包含(其实不是) 源代码意思是当c不含flag的时候把c当php文件运行 php伪协议绕过php文件执行 data://text/plain 绕…

算法汇总啊

一些常用算法汇总 算法思想-----数据结构动态规划(DP)0.题目特点1.【重点】经典例题(简单一维dp&#xff09;1.斐波那契数列2.矩形覆盖3.跳台阶4.变态跳台阶 2.我的日常练习汇总(DP)1.蓝桥真题-----路径 算法思想-----数据结构 数据结构的存储方式 : 顺序存储(数组) , 链式存储…

ubuntu安装nginx以及开启文件服务器

1. 下载源码 下载页面&#xff1a;https://nginx.org/en/download.html 下载地址&#xff1a;https://nginx.org/download/nginx-1.24.0.tar.gz curl -O https://nginx.org/download/nginx-1.24.0.tar.gz2. 依赖配置 sudo apt install gcc make libpcre3-dev zlib1g-dev ope…

轨迹规划 | 图解最优控制LQR算法(附ROS C++/Python/Matlab仿真)

目录 0 专栏介绍1 最优控制理论2 线性二次型问题3 LQR的价值迭代推导4 基于差速模型的LQR控制5 仿真实现5.1 ROS C实现5.2 Python实现5.3 Matlab实现 0 专栏介绍 &#x1f525;附C/Python/Matlab全套代码&#x1f525;课程设计、毕业设计、创新竞赛必备&#xff01;详细介绍全…

护眼台灯什么品牌好?揭秘护眼台灯十大排名

台灯作为我们日常生活中使用率较高的照明工具&#xff0c;光源的品质也是很重要的&#xff01;如果长时间使用一款质量不好的台灯&#xff0c;可能会影响我们的眼睛健康&#xff0c;特别是孩子的眼睛&#xff0c;还没有发育完全&#xff0c;影响更大。 要知道市面上很多劣质台…

IT廉连看——SpringBoot——SpringBoot快速入门

IT廉连看——SpringBoot——SpringBoot快速入门 1、idea创建工程 &#xff08;1&#xff09;普通Maven工程创建 工程名spring-boot-test 2、添加依赖 <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/PO…

理解 Golang 变量在内存分配中的规则

为什么有些变量在堆中分配、有些却在栈中分配&#xff1f; 我们先看来栈和堆的特点&#xff1a; 简单总结就是&#xff1a; 栈&#xff1a;函数局部变量&#xff0c;小数据 堆&#xff1a;大的局部变量&#xff0c;函数内部产生逃逸的变量&#xff0c;动态分配的数据&#x…

人工智能的分类有哪些

人工智能&#xff08;AI&#xff09;可以根据不同的分类标准进行分类。以下是一些常见的分类方法&#xff1a; 1. **按功能分类**&#xff1a; - 弱人工智能&#xff08;Narrow AI&#xff09;&#xff1a;也称为狭义人工智能&#xff0c;指专注于执行特定任务的AI系统&…

【蓝桥杯嵌入式】第十三届省赛(第二场)

目录 0 前言 1 展示 1.1 源码 1.2 演示视频 1.3 题目展示 2 CubeMX配置(第十三届省赛第二场真题) 2.1 设置下载线 2.2 HSE时钟设置 2.3 时钟树配置 2.4 生成代码设置 2.5 USART1 2.5.1 基本配置 2.5.2 NVIC 2.5.3 DMA 2.6 TIM 2.6.1 TIM2 2.6.2 TIM4 2.6.3 …

【Linux】 OpenSSH_9.3p1 升级到 OpenSSH_9.6p1(亲测无问题,建议收藏)

&#x1f468;‍&#x1f393;博主简介 &#x1f3c5;CSDN博客专家   &#x1f3c5;云计算领域优质创作者   &#x1f3c5;华为云开发者社区专家博主   &#x1f3c5;阿里云开发者社区专家博主 &#x1f48a;交流社区&#xff1a;运维交流社区 欢迎大家的加入&#xff01…

品牌门店稽查可调研内容

执行门店稽查的方式主要分为两类&#xff0c;明访和暗访&#xff0c;调研形式的不同&#xff0c;使得调查内容也有所差距&#xff0c;为了保证调研数据的真实性&#xff0c;目前稽查多为暗访&#xff0c;只有在需要对门店导购等的专业素养&#xff0c;或者产品库存盘点时做明访…

JAVA毕业设计134—基于Java+Springboot+Vue的社区医院管理系统(源代码+数据库+万字论文)

毕设所有选题&#xff1a; https://blog.csdn.net/2303_76227485/article/details/131104075 基于JavaSpringbootVue的社区医院管理系统(源代码数据库万字论文)134 一、系统介绍 本项目前后端分离&#xff0c;分为管理员、用户、医生、前台四种角色 1、用户&#xff1a; 注…

有大学老师正用ChatGPT批改论文,让同学也这么做!

4月7日&#xff0c;CNN消息&#xff0c;美国伊萨卡学院-战略传播学教授Diane Gayeski&#xff0c;正在使用ChatGPT批改学生的论文。 当Diane收到学生提交的论文时&#xff0c;会将部分内容输入到ChatGPT&#xff0c;然后让其进行评分并给出详细的修改建议。 Diane也会让班里的…

js控制字数

效果图 js方法 // 控制字数新闻**描述**字数 const delNews () >{let limit 80;if(allObj.newsList && allObj.newsList.length > 0){allObj.newsList.forEach(item>{if(item.contentDescribe.length>80){item.contentDescribeitem.contentDescribe.sub…

mac系统内存(RAM)清理方法,一键式清理工具推荐!

在现代计算机体系结构中&#xff0c;运行内存&#xff08;RAM&#xff09;扮演着关键角色&#xff0c;是保证系统流畅高效运作的核心组件之一。RAM作为一种瞬态存储媒介&#xff0c;专门用来存放当前活跃的程序及其相关的临时数据。不同于硬盘驱动器&#xff08;HDD&#xff09…