C++设计模式_12_Singleton 单件模式

news2024/9/27 5:45:52

在之前的博文C++57个入门知识点_44:单例的实现与理解中,已经详细介绍了单例模式,并且根据其中内容,单例模式已经可以在日常编码中被使用,本文将会再做梳理。
Singleton 单件模式可以说是最简单的设计模式,但由于多线程环境的双检查锁里的内存reorder的问题,实现时的细节并不简单,大家需要注意多线程环境下的安全做法。为了整体的一致性,本篇都称之为单件模式。

文章目录

  • 1. “对象性能”模式
    • 1.1 典型模式
  • 2. 动机(Motivation)
  • 3. Singleton 单件模式的实现版本
    • 3.1 版本1:线程非安全版本
    • 3.2 版本2:线程安全版本,但锁的代价过高
    • 3.3 版本3:双检查锁
    • 3.4 版本4:C++ 11版本之后的跨平台实现
    • 3.5 总结
  • 4. 模式定义
  • 5. 结构
  • 6. 要点总结
  • 7. 其他参考

单件模式属于一个新的类别,将其归结到“对象性能”模式,该模式的简介如下:

1. “对象性能”模式

面向对象很好地解决了“抽象”的问题,但是不可避免地要付出一定的代价(虚函数(内存等代价哒)、继承)。对于通常情况来讲,面向对象的成本大都可以忽略不计。但是某些情况(倍乘效应等),面向对象所带来的成本必须谨慎处理。

1.1 典型模式

  • Singleton
  • Flyweight

2. 动机(Motivation)

  • 在软件系统中,经常有这样一些特殊的类,必须保证它们在系统中只存在一个实例,才能确保它们的逻辑正确性、以及良好的效率
  • 如何绕过常规的构造器,提供一种机制来保证一个类只有一个实例?
  • 这应该是类设计者的责任,而不是使用者的责任

3. Singleton 单件模式的实现版本

首先将构造函数和拷贝构造函数设置为私有,如果不写的话,编译器会默认生成两个公有的,让外界不能使用它们,唯一的做法就是将其设置为私有的。然后再设置静态变量,并进行静态变量的初始化。代码如下

class Singleton{
private:
    Singleton();
    Singleton(const Singleton& other);
public:
    static Singleton* getInstance();
    static Singleton* m_instance;
};

3.1 版本1:线程非安全版本

先来看第一种写法,第一次调用的时候,m_instance == nullptr成立的情况下m_instance = new Singleton();创建对象,返回m_instance,第二次调用的话,m_instance == nullptr不成立,继续返回原先的对象,这样就可以保证永远只返回第一次的对象。

//线程非安全版本
Singleton* Singleton::getInstance() {
    if (m_instance == nullptr) {
        m_instance = new Singleton();
    }
    return m_instance;
}

上述的版本会有其他问题,它在单线程下是OK的,但是多线程环境下就不行了。假设我有2个thread,threadA进到if (m_instance == nullptr)判断第一次调用,还没执行下一行,threadB假如此时得到时间片,开始执行if (m_instance == nullptr)也是成立,就会执行下一行,此时再跳回threadA,此时就会继续执行下一行,这样就会产生2个对象,因此这是线程非安全的。为了实现多线程安全如何去做呢?

3.2 版本2:线程安全版本,但锁的代价过高

第一种常见做法:加锁

//线程安全版本,但锁的代价过高
Singleton* Singleton::getInstance() {
    Lock lock; //锁的局部变量,出作用域之后消失,释放锁
    if (m_instance == nullptr) {
        m_instance = new Singleton();
    }
    return m_instance;
}

TreadA进到Lock lock;开始加锁,往下执行时,另外一个线程threadB进来,但是TreadA已经获取到锁,仍未释放,threadB就需要一直等到TreadA执行完函数释放锁。等threadB进去之后m_instance == nullptr就不成立了。

3.3 版本3:双检查锁

上一种版本实现了锁的安全,但是锁的代价太高。假如对象已经创建,那么有两个线程同时访问,threadA执行到Lock lock;,threadB是无法进入的,threadB是一个读操作,当存在多个线程的情况下,读操作是不需要加锁的。读操作是没有安全问题的,这个时候加锁对于读操作的线程是一种等待时间上的浪费。假如高并发场景下,web服务是典型高并发,10万人同时访问时,锁对于读操作代价也会是十分高的。

所以发展出一种新的实现形式,double check lock 双检查锁,之前的版本是不管什么情况,先锁,现在是if(m_instance==nullptr)先问下变量是否为空值,如果为空才加锁,如果不空就不需要加锁,直接返回即可,加完锁之后再判断是否为空。这样就在读操作时减少了时间代价。

//双检查锁,但由于内存读写reorder不安全
Singleton* Singleton::getInstance() {
    
    if(m_instance==nullptr){
        Lock lock;
        if (m_instance == nullptr) {
            m_instance = new Singleton();
        }
    }
    return m_instance;
}

有人会考虑,第二个判断if (m_instance == nullptr) 是不是可以去掉,我们假设2个线程,threadA经过第一个判断进来,然后在执行Lock lock;之前,threadB也会进来到Lock lock;之前,两个线程即使加锁也就会等一等,都会执行代码m_instance = new Singleton();,这样就会有2个对象。第二个判断就是为了放置这种情况的出现。

上面的版本看起来就是没有问题的了,也相当一段时间内迷惑了计算机领域的专家,大概2000年时才被JAVA领域的专家发现漏洞:内存读写reorder导致双检查锁的失效。

什么是reorder,正常情况下,大家看到的代码会有指令序列,那么大家通常会认为,指令序列会按照你所想象的方式执行,但是实际上整个代码到了CPU的指令层次(线程是在指令层次抢时间片),指令和我们很多时候的假设很可能不一样。比如: m_instance = new Singleton();一般会有以下的假设,拆分成3个步骤,第一步是先分配内存,第二步是调用构造器(对分配的内存进行初始化),第三步把内存的地址(指针)给m_instance。但是这三步是我们假想的,但是在指令级别,很可能被编译器优化reorder,在reorder之后可能就会先第一步,第三步,最后再第三步。很多研究者在不同的CPU上做实验发现很多语言都会这种现象,这样一来就有些乱套。按照可能的reorder顺序,先分配内存,直接就赋值,m_instance就不是nullptr了,构造器还没调用,另一个线程做第一次判断时发现m_instance不是nullptr,直接返回一个对象,返回的这个对象还没有经过构造器处理,其状态不对,是不可用的。

3.4 版本4:C++ 11版本之后的跨平台实现

为了解决上面的问题,java和c#语言就加了volatile的关键字来保证按照我们的想法来执行。VC++中自己也加了volatile的关键字。

C++11之后才有了这样的机制,可以帮助可以在语言层面实现跨平台的实现。

//C++ 11版本之后的跨平台实现 (volatile)
std::atomic<Singleton*> Singleton::m_instance; //声明原子对象
std::mutex Singleton::m_mutex;

Singleton* Singleton::getInstance() {
    Singleton* tmp = m_instance.load(std::memory_order_relaxed);//得到tmp指针,帮助实现屏蔽编译器的reorder
    std::atomic_thread_fence(std::memory_order_acquire);//获取内存fence,内存reorder的屏障
    //tmp就不会被reorder,底下为双检查锁的操作
    if (tmp == nullptr) {
        std::lock_guard<std::mutex> lock(m_mutex);
        tmp = m_instance.load(std::memory_order_relaxed);
        if (tmp == nullptr) {
            tmp = new Singleton;
            std::atomic_thread_fence(std::memory_order_release);//释放内存fence
            m_instance.store(tmp, std::memory_order_relaxed);
        }
    }
    return tmp;
}

经过上面的操作,才在C++11之后实现跨平台的实现。

3.5 总结

总体来说,如果是单线程使用版本1已经是足够好了;如果是多线程,使用版本2是OK的,但是效率不高;双检查锁在不使用volatile不能用,是很容易出现问题的;C++11版本之后跨平台实现双检查锁就使用的是最后一个版本

4. 模式定义

保证一个类仅有一个实例,并提供一个该实例的全局访问点
–《设计模式》GoF

5. 结构

在这里插入图片描述

6. 要点总结

  • Singleton模式中的实例构造器可以设置为protected以允许子类派生
  • Singleton模式一般不要支持拷贝构造函数和Clone接口,因为这有可能导致多个对象实例,与Singleton模式的初衷违背。
  • 如何实现多线程环境下安全的Singleton?注意对双检查锁的正确实现。

7. 其他参考

C++设计模式——单例模式

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

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

相关文章

力扣第1005题 K 次取反后最大化的数组和 c++ 贪心 双思维

题目 1005. K 次取反后最大化的数组和 简单 相关标签 贪心 数组 排序 给你一个整数数组 nums 和一个整数 k &#xff0c;按以下方法修改该数组&#xff1a; 选择某个下标 i 并将 nums[i] 替换为 -nums[i] 。 重复这个过程恰好 k 次。可以多次选择同一个下标 i 。 以…

【Javascript】通过浏览器书签构建与执行自动刷新脚本

0x00 前言 日常工作中&#xff0c;经常遇到需要时不时点一下刷新这样的事情&#xff08;怪前端想不到写一个自动刷新&#xff09; 但是…… 没有自动刷新按钮&#xff0c;在这页面手动点刷新还是太浪费时间了。 有时候懒得等了去做别的事情&#xff0c;过一小时回来刷新一下&a…

k8s之Flannel网络插件安装提示forbidden无权限

一、问题描述 在安装k8s的网络插件时&#xff0c;提示如下信息&#xff0c;各种forbidden无权限 [rootzzyk8s01 scripts]# kubectl apply -f kube-flannel.yml Error from server (Forbidden): error when retrieving current configuration of: Resource: "policy/v1b…

Vue中使用Web Serial API连接串口,实现通信交互

Vue中使用Web Serial API连接串口&#xff0c;实现通信交互 Web Serial API&#xff0c;web端通过串口与硬件通信; 该API是JS本身 navigator 对象上就独有的&#xff0c;所以与Vue和React框架开发都没有太大的关系&#xff0c; 串口是一个双向通信接口&#xff0c;允许字节发送…

ES6 模块化编程 详解

目录 一、基本介绍 二、基本原理示意图 三、传统CommonJS实现模块化编程 1.介绍 : 2.实例 : 四、ES6新特性实现模块化编程 1.介绍 : 2.实例 : 一、基本介绍 (1) ES6新特性——模块化编程&#xff0c;用于解决传统非模块化开发中出现的"命名冲突", "文件…

LDAP和Kerberos疑难问题诊断方法

不同的工具和方法总能给问题的解决带来希望。本文使用SSSD工具诊断Kerberos和LDAP登录问题诊断。后端&#xff08;通常也称为数据提供程序&#xff09;是管理和创建缓存的 SSSD 子进程。此过程与LDAP服务器通信&#xff0c;执行不同的查找查询并将结果存储在缓存中。它还针对 L…

PY32F002A系列单片机:高性价比、低功耗,满足多样化应用需求

PY32F002A系列微控制器是一款高性能、低功耗的MCU&#xff0c;它采用32位ARM Cortex-M0内核&#xff0c;最高工作频率达到24MHz&#xff0c;提供了强大的计算能力。此外&#xff0c;PY32F002A拥有最大20Kbytes的flash存储器和3Kbytes的SRAM&#xff0c;为简单的数据处理提供了充…

一文速通Nginx网关与gateway网关区分

目录 API网关介绍 gateway基本介绍 Nginx基本介绍 Nginx与API gateway网关 API网关介绍 网关的角色是作为一个 API 架构&#xff0c;用来保护、增强和控制对于 API 服务的访问。API 网关是一个处于应用程序或服务&#xff08;提供 REST API 接口服务&#xff09;之前的系…

创建 Edge 浏览器扩展教程(上)

创建 Edge 浏览器扩展教程&#xff08;上&#xff09; 介绍开始之前后续步骤开始之前1&#xff1a;创建清单 .json 文件2 &#xff1a;添加图标3&#xff1a;打开默认弹出对话框 介绍 在如今日益数字化的时代&#xff0c;浏览器插件在提升用户体验、增加功能以及改善工作流程方…

Spring Authorization Server 1.1 扩展 OAuth2 密码模式与 Spring Cloud Gateway 整合实战

目录 前言无图无真相创建数据库授权服务器maven 依赖application.yml授权服务器配置AuthorizationServierConfigDefaultSecutiryConfig 密码模式扩展PasswordAuthenticationTokenPasswordAuthenticationConverterPasswordAuthenticationProvider JWT 自定义字段自定义认证响应认…

【AI视野·今日Robot 机器人论文速览 第五十八期】Thu, 19 Oct 2023

AI视野今日CS.Robotics 机器人学论文速览 Thu, 19 Oct 2023 Totally 25 papers &#x1f449;上期速览✈更多精彩请移步主页 Daily Robotics Papers InViG: Benchmarking Interactive Visual Grounding with 500K Human-Robot Interactions Authors Hanbo Zhang, Jie Xu, Yuch…

『heqingchun-Qt的艺术-优雅界面设计开发』

Qt的艺术-优雅界面设计开发 效果图 一、新建Qt窗口工程 二、准备资源文件 1.图标资源 链接: 图标资源 2.Qss资源 链接: Qss资源 三、设计开发 项目源码链接: CSDN资源

随机生成一个指定边数多边形

随机生成一个指定边数多边形 算法背景&#xff1a;我们想完成一个可以随机生成指定边数多边形的算法。在生成过程中&#xff0c;需要避免随机点连接过程中交叉的问题。 算法步骤   1、为了随机生成一个n边形&#xff0c;我们先随机生成n个点。例如下图&#xff0c;我们随机生…

AI Chatbot 对企业降低人力成本和提高竞争力的作用

随着人工智能技术的发展&#xff0c;越来越多的企业开始尝试将AI ChatBot引入到业务中。AI Chatbot是一种基于人工智能技术的智能对话机器人。它可以模拟人类对话&#xff0c;通过自然语言处理和机器学习算法来理解和回答用户的问题&#xff0c;可以帮助企业降低成本和提高效率…

ExoPlayer架构详解与源码分析(7)——SampleQueue

系列文章目录 ExoPlayer架构详解与源码分析&#xff08;1&#xff09;——前言 ExoPlayer架构详解与源码分析&#xff08;2&#xff09;——Player ExoPlayer架构详解与源码分析&#xff08;3&#xff09;——Timeline ExoPlayer架构详解与源码分析&#xff08;4&#xff09;—…

上海亚商投顾:沪指放量反弹 两市超4500股飘红

上海亚商投顾前言&#xff1a;无惧大盘涨跌&#xff0c;解密龙虎榜资金&#xff0c;跟踪一线游资和机构资金动向&#xff0c;识别短期热点和强势个股。 一.市场情绪 三大指数昨日集体反弹&#xff0c;深成指、创业板指盘中涨超1%&#xff0c;黄白二线大幅分化&#xff0c;题材…

视频配音软件有哪些?推荐几款好用的视频配音软件

视频配音软件有哪些&#xff1f;推荐几款好用的视频配音软件 不知道大家有没有这样的体会&#xff0c;我们在观看视频作品的时候&#xff0c;如果没有声音&#xff0c;就会少了很多韵味和可供思索的空间&#xff0c;相反&#xff0c;好的配音可以为作品增色&#xff0c;一句台…

docker 搭建 flink 并上传任务

文章目录 一、docker 搭建 flink1、选择合适的 flink 版本2、重新创建 JobManager、TaskManager 容器并挂载配置文件 二、flink 简单示例1、创建项目架构2、批处理简单示例3、流处理简单示例4、上传 flink 集群①、UI 界面提交任务②、命令提交任务 5、web-ui 提交查看撤销任务…

区块链技术与应用 【全国职业院校技能大赛国赛题目解析】第五套区块链系统部署与运维

第五套区块链系统部署与运维题目 环境 : ubuntu20 fisco : 2.8.0 子任务1-2-1: 登陆Linux服务器,安装并部署下图所示的单机、四机构、三群组、八节点的星形组网拓扑区块链系统,具体工作内容如下 此题在官网有例子如图: 每个机构拥有两个节点,机构A属于中心,属于群组1,…

Linux文件系统 struct dentry 结构体解析

文章目录 前言一、目录项简介二、struct dentry2.1 简介2.2 dentry和inode关联2.3 目录项状态2.4 目录项特点 三、dentry cache3.1 简介3.2 dentry cache 初始化3.3 dentry cache 查看 四、dentry与mount、file的关联五、其他参考资料 前言 这两篇文章介绍了: VFS 之 struct f…