C++多线程:单例模式与共享数据安全(七)

news2024/11/25 10:44:36
1、单例设计模式
  • 单例设计模式,使用的频率比较高,整个项目中某个特殊的类对象只能创建一个

  • 并且该类只对外暴露一个public方法用来获得这个对象。

  • 单例设计模式又分懒汉式和饿汉式,同时对于懒汉式在多线程并发的情况下存在线程安全问题

    • 饿汉式:类加载的准备阶段就会将static变量、代码块进行实例化,最后只暴露一个public方法获得实例对象。

    • 懒汉式:当需要用到的时候再去加载这个对象。这时多线程的情况下可能存在线程安全问题

  • 对于饿汉式这里不做具体的解释,本节只讨论多线程与懒汉式的线程安全问题

2、单线程下的懒汉模式
2.1、单例对象的创建:
  • 将类指针对象进行静态私有化,并且在类外初始化这个对象为空;静态能保证的是这个对象属于这个类不属于任何一个对象
  • 私有化空构造器防止可以实例化对象
  • 对外暴露一个public方法获取该对象,如果在获取时发现该对象为空,那么进行实例化,否则直接返回
  • 因此可以看到实例化只有一次,多次获取到的对象的地址属于同一个
class Single_Instance {
private:
    static Single_Instance *instance;
    Single_Instance() {

    }
public:
    static Single_Instance *get_Instance(){
        if(instance == NULL){
            instance = new Single_Instance();
        }
        return instance;
    }
    void func(){
        std::cout << "func(), &instance = " << instance << std::endl;
    }
};
Single_Instance *Single_Instance::instance = NULL;

void test1()
{
    Single_Instance *instance1 = Single_Instance::get_Instance();
    Single_Instance *instance2 = Single_Instance::get_Instance();
    instance1->func();
    instance2->func();
}

# 输出
func(), &instance = 0x5652eefede70
func(), &instance = 0x5652eefede70
2.2、单例对象的析构
  • 很明显上面的代码缺少一个析构函数,并且似乎无从下手找一个合适的时机对其进行析构,只能等待程序运行结束操作系统回收?
  • 其实可以通过内部类的方式进行析构
    • 首先在单例类内部进行私有化一个内部类
    • 对外暴露的public获取instance的对象接口在new实例化对象的时候创建一个内部类静态成员
    • 内部类静态成员的好处是只有一份
    • 当作用域结束时内部类就会负责析构掉主类的静态成员对象
class Single_Instance {
private:
    static Single_Instance *instance;
    Single_Instance() {

    }
    class inner_class {
    public:
        ~inner_class(){
            if(Single_Instance::instance){
                delete Single_Instance::instance;
                Single_Instance::instance = NULL;
                std::cout << "inner_class::~inner_class(), 析构Single_Instance::instance对象" << std::endl;
            }
        }
    };
public:
    static Single_Instance *get_Instance(){
        if(instance == NULL){
            instance = new Single_Instance();
            static inner_class innerClass;
        }
        return instance;
    }
    void func(){
        std::cout << "func(), &instance = " << instance << std::endl;
    }
};
Single_Instance *Single_Instance::instance = NULL;

void test1()
{
    Single_Instance *instance1 = Single_Instance::get_Instance();
    Single_Instance *instance2 = Single_Instance::get_Instance();
    instance1->func();
    instance2->func();
}
#输出
func(), &instance = 0x558eb768de70
func(), &instance = 0x558eb768de70
inner_class::~inner_class(), 析构Single_Instance::instance对象
3、单例模式与多线程
  • 单例模式的对象可能会被多个线程使用,但是又必须保证这个单例的对象只有一份

  • 不能重复创建、也必须保证这个对象在多线程使用过程中不会因为创建而产生数据安全问题,即多线程抢占的创建这一个对象

class Single_Instance {
private:
    static Single_Instance *instance;
    Single_Instance() {

    }
    class inner_class {
    public:
        ~inner_class(){
            if(Single_Instance::instance){
                delete Single_Instance::instance;
                Single_Instance::instance = NULL;
                std::cout << "inner_class::~inner_class(), 析构Single_Instance::instance对象" << std::endl;
            }
        }
    };
public:
    static Single_Instance *get_Instance(){
        if(instance == NULL){
            instance = new Single_Instance();
            static inner_class innerClass;
        }
        return instance;
    }
    void func(){
        std::cout << "func(), &instance = " << instance << std::endl;
    }
};
Single_Instance *Single_Instance::instance = NULL;

void thread_func()
{
    std::cout << "子线程开始执行了" << std::endl;
    Single_Instance *instance = Single_Instance::get_Instance();
    std::cout << "thread_func, &instance = " << instance << std::endl;
    std::cout << "子线程执行结束了" << std::endl;
}

void test2()
{
    std::thread mythread1(thread_func);
    std::thread mythread2(thread_func);
    std::thread mythread3(thread_func);
    std::thread mythread4(thread_func);
    mythread1.join();
    mythread2.join();
    mythread3.join();
    mythread4.join();
}

在这里插入图片描述
可以看到实例化不止一个单例对象,这一现象违反了单例的思想,因此需要在多线程抢占创建时进行互斥(mutex)

3.1、解决方案(一)
  • 使用互斥量的方式,对线程访问获取对象进行阻塞
  • 但是不难发现问题,其实这个对象只创建一次,之后的访问单纯的获取这个对象也要进行加锁逐个排队访问临界区,这一现象导致效率极低
std::mutex mutex_lock;
class Single_Instance {
private:
    static Single_Instance *instance;
    Single_Instance() {

    }
    class inner_class {
    public:
        ~inner_class(){
            if(Single_Instance::instance){
                delete Single_Instance::instance;
                Single_Instance::instance = NULL;
                std::cout << "inner_class::~inner_class(), 析构Single_Instance::instance对象" << std::endl;
            }
        }
    };
public:
    static Single_Instance *get_Instance(){
        std::unique_lock<std::mutex> uniqueLock(mutex_lock);
        if(instance == NULL){
            instance = new Single_Instance();
            static inner_class innerClass;
        }
        return instance;
    }
    void func(){
        std::cout << "func(), &instance = " << instance << std::endl;
    }
};
Single_Instance *Single_Instance::instance = NULL;

void thread_func()
{
    std::cout << "子线程开始执行了" << std::endl;
    Single_Instance *instance = Single_Instance::get_Instance();
    std::cout << "thread_func, &instance = " << instance << std::endl;
    std::cout << "子线程执行结束了" << std::endl;
}

void test2()
{
    std::thread mythread1(thread_func);
    std::thread mythread2(thread_func);
    std::thread mythread3(thread_func);
    std::thread mythread4(thread_func);
    mythread1.join();
    mythread2.join();
    mythread3.join();
    mythread4.join();
}
3.2、解决方式(二)

双重检查机制(DCL)进行绝对安全解决

  • 双重检查:
    • 首先在锁外面加入一个if判断,判断这个对象是否存在,如果存在就没有必要上锁创建,直接返回即可
    • 如果对象不存在,首选进行加锁,然后在if判断对象是否存在,这个if的意义在于当多个线程阻塞在mutex锁头上时
    • 突然有一个线程1创建好了,那么阻塞在mutex锁头上的线程2、3、4…都不用再继续创建,因此在加一个if判断

这里还需要解释一下volatile关键字:

  • volatile关键字的作用是防止cpu指令重排序,重排序的意思就是干一件事123的顺序,cpu可能重排序为132

  • 为什么需要防止指令重排序,因为对象的new过程分为三部曲:

    (1)分配内存空间、(2)执行构造方法初始化对象、(3)将这个对象指向这个空间;

    由于程序运行CPU会进行指令的重排序,如果执行的指令是132顺序,A线程执行完13之后并没有完成对象的初始化、而这时候转到B线程;B线程认为对象已经实例化完毕、其实对象并没有完成初始化!产生错误

  • 但这个问题在C++11中已经禁止了重排序,因此不需要使用volatile关键字,但在Java和一些其他语言中可能有,Java中这个关键字是针对即时编译器JIT进行指令重排序的

static Single_Instance *get_Instance(){
    if(instance == NULL){
        std::unique_lock<std::mutex> uniqueLock(mutex_lock);
        if(instance == NULL){
            instance = new Single_Instance();
            static inner_class innerClass;
        }
    }
    return instance;
}

只需要把上面的代码改成这个样子即可

4、std::call_once()
  • std::call_once()是C++11引入的函数,该函数的功能就是保证一个方法只会被调用一次。

    • 参数二:一个函数名func

    • 参数一:std::once_flag一个标记,本质是一个结构体。该标志可以用于标记参数二该函数是否已经调用过了

    • 参数三:参数二函数的参数

  • std::call_once()具有互斥量的这种能力,且效率上比mutex互斥量效率更高,因此也可以使用这个函数对单例的线程安全进行保证

    • 当call_once调用过一次之后,std::once_flag将会被修改标记(已调用),那么之后都不会在调用
  • 下面看个代码举例,可以看到create_Instance()函数中对于这个函数只执行了一次,完全ojbk。

class Single_Instance {
private:
    static Single_Instance *instance;
    static std::once_flag instance_flag;
    Single_Instance() {

    }
    class inner_class {
    public:
        ~inner_class(){
            if(Single_Instance::instance){
                delete Single_Instance::instance;
                Single_Instance::instance = NULL;
                std::cout << "inner_class::~inner_class(), 析构Single_Instance::instance对象" << std::endl;
            }
        }
    };
public:

    static void create_Instance(){
        instance = new Single_Instance();
        static inner_class innerClass;
    }
    static Single_Instance *get_Instance(){
        std::call_once(instance_flag, create_Instance);
        return instance;
    }
    void func(){
        std::cout << "func(), &instance = " << instance << std::endl;
    }
};
Single_Instance *Single_Instance::instance = NULL;
std::once_flag Single_Instance::instance_flag;

void thread_func()
{
    std::cout << "子线程开始执行了" << std::endl;
    Single_Instance *instance = Single_Instance::get_Instance();
    std::cout << "thread_func, &instance = " << instance << std::endl;
    std::cout << "子线程执行结束了" << std::endl;
}

void test3()
{
    std::thread mythread1(thread_func);
    std::thread mythread2(thread_func);
    std::thread mythread3(thread_func);
    std::thread mythread4(thread_func);
    mythread1.join();
    mythread2.join();
    mythread3.join();
    mythread4.join();
}

在这里插入图片描述

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

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

相关文章

JUC:原子类型的使用(原子整数、原子引用、原子数组、字段更新器、累加器)

文章目录 原子类型AtomicInteger 原子整数AtomicReferenc 原子引用AtomicStampedReference 带版本号的原子引用AtomicMarkableReference 仅记录是否修改的原子引用AtomicXXXArray 原子数组AtomicXXXFieldUpdater 字段更新器LongAdder累加器 原子类型 AtomicInteger 原子整数 …

Web Animations API 动画

Element.animate() dom.animate动画可以避免污染dom原有的css动画 参考资料 Element.animate() - Web API 接口参考 | MDN Element: getAnimations() method - Web APIs | MDN .tunnel{width:200px;height:200px;background-color:#38f;}<div class"tunnel" …

文件夹0字节:原因、恢复与防范全攻略

在我们日常使用电脑的过程中&#xff0c;有时候会遇到一种奇怪的现象&#xff1a;某个原本包含大量文件的文件夹突然变成了0字节。这种突如其来的变化令人措手不及&#xff0c;不禁让人担心重要的数据是否已经彻底丢失。那么&#xff0c;文件夹0字节到底是怎么回事&#xff1f;…

氟化氢冷凝装置配套PFA烧瓶PFA冷凝管PFA接收瓶等

一、装置清单及说明&#xff1a; 1. PFA烧瓶 材质为PFA&#xff0c;半透明&#xff0c;耐受强酸强碱&#xff0c;常用500ml 1000ml&#xff0c;其他规格等可自行选择&#xff0c;若需要3颈及以上建议选择500ml以上规格&#xff0c;可根据要求选择有液位计&#xff0c;可看出瓶…

Redis 应用问题解决——缓存穿透、缓存击穿、缓存雪崩、分布式锁

缓存穿透 key对应的数据在数据源不存在&#xff0c;每次针对此key的请求从缓存获取不到&#xff0c;请求都会压到数据源&#xff0c;从而可能压垮数据源。比如用一个不存在的用户id获取用户信息&#xff0c;不论缓存还是数据库都没有&#xff0c;若黑客利用此漏洞进行攻击可能…

大屏可视化项目示例--基于Vue3+vite2+echart+mock+axios+dataV

图例&#xff1a; 项目环境&#xff1a; Vite、Echarts、Npm、Node、axios、mock、vue3、dataV。 项目地址&#xff1a; IofTV-Screen-Vue3: &#x1f525;(IofTV-Screen Vue3版本)一个基于 vue3、vite、Echart 框架的物联网可视化&#xff08;大屏展示&#xff09;模板&…

状态压缩DP

哈密顿路径问题&#xff1a; 一般设 表示 状态下&#xff0c;为最后一个最值情况 。 一般有两种稍微不同的写法&#xff0c;单纯就是写法不同&#xff0c;思路方法都相同。 第一个例题为第一种转移方法&#xff0c;有当前转移后面。 后面的都是由前面转移目前。 G. Shuff…

学习网安(19)

防火墙——安全产品 功能&#xff1a; 杀毒&#xff1a; 针对病毒&#xff0c;特征篡改系统中的文件 杀毒软件针对处理病毒程序 防火墙&#xff1a; 针对木马&#xff0c;特征系统窃密 防火墙针对处理木马 种类&#xff1a; 硬件防火墙&#xff1a; 各个网络安全厂商…

PCL点云库出现错误:..\dist.h(523): error C3861: “pop_t”: 找不到标识符

工程代码&#xff1a;简单地测试了k-d树的最近邻搜索功能 #include<pcl/point_cloud.h> #include<pcl/kdtree/kdtree_flann.h>#include<iostream> #include<vector> #include<ctime>using namespace std;int main(int argc, char** argv) {//使…

2024第八届全国青少年无人机大赛暨中国航空航天科普展览会

2024第八届全国青少年无人机大赛暨中国航空航天科普展览会 邀请函 主办单位&#xff1a; 中国航空学会 重庆市南岸区人民政府 招商执行单位&#xff1a; 重庆港华展览有限公司 为更好的培养空航天产业人才&#xff0c;汇聚航空教育产业创新科技&#xff0c;丰富和完善航…

【正点原子Linux连载】 第二十五章HDMI屏幕驱动实验摘自【正点原子】ATK-DLRK3568嵌入式Linux驱动开发指南

1&#xff09;实验平台&#xff1a;正点原子ATK-DLRK3568开发板 2&#xff09;平台购买地址&#xff1a;https://detail.tmall.com/item.htm?id731866264428 3&#xff09;全套实验源码手册视频下载地址&#xff1a; http://www.openedv.com/docs/boards/xiaoxitongban 第二十…

C++的并发世界(二)——初识多线程

0.引言 C的并发世界&#xff08;零&#xff09;和C的并发世界&#xff08;一&#xff09;的东西真的对于我这种初学者难以理解&#xff0c;我确定从第一个多线程案例进行学习归纳总结。 1.多线程的目的 ①将耗时的任务进行分解&#xff0c;进行实时响应;   ②充分利用多核CP…

go连接数据库(原生)

根据官网文档 Go Wiki: SQL Database Drivers - The Go Programming Language 可以看到go可以连接的关系型数据库 ​ 常用的关系型数据库基本上都支持&#xff0c;下面以mysql为例 下载mysql驱动 打开上面的mysql链接 GitHub - go-sql-driver/mysql: Go MySQL Driver i…

文献研读|AIGC溯源场景及研究进展

前言&#xff1a;本文介绍关于AIGC生成阶段针对不同溯源场景的几篇相关工作。 如下图所示&#xff0c;在AIGC生成阶段&#xff0c;有4种溯源场景&#xff1a; 1&#xff09;生成模型溯源训练数据 2&#xff09;微调模型溯源预训练模型 3&#xff09;AIGC溯源训练数据/训练概念…

休闲娱乐代理售票系统的设计与实现|Springboot+ Mysql+Java+ B/S结构(可运行源码+数据库+设计文档)折扣票,退票信息,订单信息,出票信息,公告信息,个人中心

本项目包含可运行源码数据库LW&#xff0c;文末可获取本项目的所有资料。 推荐阅读300套最新项目持续更新中..... 最新ssmjava项目文档视频演示可运行源码分享 最新jspjava项目文档视频演示可运行源码分享 最新Spring Boot项目文档视频演示可运行源码分享 2024年56套包含ja…

2024年全新靠谱的FTP替代升级解决方案

随着企业规模的扩大和业务的多元化&#xff0c;传统的TCP协议在数据传输效率上逐渐显现出局限性。TCP协议虽然以其稳定性和可靠性被广泛应用&#xff0c;但在面对大规模数据传输时&#xff0c;其性能瓶颈逐渐成为企业发展的阻碍。同时&#xff0c;基于TCP的应用协议如Telnet、F…

【采购季即将结束】阿里云 腾讯云 部分性价比机型价格回调下架 抓住采购季尾声!游戏云服务器价格对比表 已更新!低至50/年

更新日期&#xff1a;4月3日&#xff08;阿里云价格回调 6个月无折扣&#xff0c;京东云采购季持续进行&#xff09; 本文纯原创&#xff0c;侵权必究 【云服务器推荐】价格对比&#xff01;阿里云 京东云 腾讯云 选购指南视频截图 《最新对比表》已更新在文章头部—腾讯云文…

关于 ulimit 的两个天坑

稍微有点 Linux 经验的人一定会遇到过 “Too many open files” 错误&#xff0c;这个错误本质是 ulimit 设置不合理导致的。关于 ulimit 设置&#xff0c;有哪些需要注意的点呢&#xff1f;本文给大家做一个介绍&#xff0c;希望对大家有所帮助。 如何确认 ulimit 设置生效了…

Redis热点Key问题分析与解决

目录 一、问题现象描述 二、什么是热点Key 三、热点Key的危害 3.1 Redis节点负载过高 3.2 Redis集群负载不均 3.3 Redis集群性能下降 3.4 数据不一致 3.5 缓存击穿 四、热点Key产生的原因分析 4.1 热点数据 4.2 业务高峰期 4.3 代码逻辑问题 五、如何检测热点Key …

redis 缓存穿透 击穿 雪崩 的原因及解决方法

一 总结 1&#xff0c;原因及解决办法 ① 缓存穿透&#xff1a;大量请求根本不存在的key&#xff08;下文详解&#xff09; ② 缓存雪崩&#xff1a;redis中大量key集体过期&#xff08;下文详解&#xff09; ③ 缓存击穿&#xff1a;redis中一个热点key过期&#xff08;大量…