目录

一、版本一:禁用构造与拷贝

二、版本二:注册析构函数/嵌套垃圾回收

(1)使用atexit注册程序结束时的函数

(2)使用对象嵌套垃圾回收

三、版本三:线程安全

四、版本四:编译器、CPU指令重排问题

五、版本五:局部静态变量线程安全性

六、版本六:模板提高复用率


        单例模式是C++中最常用的一种设计模式,他确保无论在单线程还是多线程中,都只会有一个实例对象,并提供一个全局访问点。这种模式在配置管理,日志记录,设备驱动等场景中非常有用。本文将从思维迭代的方式,一步步完善单例模式,方便大家理解和记忆。

        分析一个设计模式,通常要从稳定点和变化点入手。对于单例模式其稳定点显然是一个类要提供一个一个全局的访问方式,且不允许外部任意构造、拷贝。而变化点理论上是没有的,但是我们强行认为变化点在于使用继承+模板来扩展单例模式。

一、版本一:禁用构造与拷贝

        既然单例模式只能提供一个全局访问点,且不能让其他人随意创建,那么很显然需要禁用其构造函数、拷贝构造等。当然这里是懒汉模式,如果你采取饿汉模式,直接在静态区创建单例对象,而非创建单例指针,则可以避免析构调不到的问题。下面我们来分析一下:

class Singleton
{
public://获取全局访问点static Singleton* GetInstance(){if (_instance==nullptr){_instance = new Singleton();}return _instance;}private:static Singleton* _instance;	//全局访问点//私有化各种构造函数,防止外面任意创建对象
private:Singleton() {};		//构造~Singleton() {};	//析构Singleton(const Singleton&) = delete;		//拷贝构造Singleton& operator=(const Singleton&) = delete;		//赋值运算符重载Singleton(Singleton&&) = delete;		//移动构造Singleton& operator=(Singleton&&) = delete;		//移动赋值运算符重载
};
Singleton* Singleton::_instance = nullptr;	//初始化静态成员

        我们可以看到这个代码存在一些问题。比如他不能自动调用析构函数(即使我们把析构函数public,再手动调用也会出现信号等情况没有执行到这里就退出了),因为单例对象的虽然是创建在堆上的,但是其指针在全局静态区。

        当程序声明周期到达、或者以外收到信号退出的时候,该进程的地址空间虽然会被操作系统回收,仅仅会对这个指针销毁,无法析构其指向的内容(堆上的对象必须要手动调用delete才会被析构)。

        既然无法调用到析构函数,那么其析构的执行流也无法被执行。当他的析构函数涉及到文件操作、网络连接等资源。比如关闭文件描述符、刷新文件缓冲区到内核态时就会出问题。举个例子:日志对象是一个单例对象,他打开了一系列文件,正常情况下手动调用析构函数会正常关闭文件描述符,而关闭文件描述符是一个把用户态文件缓冲区刷新到内核态的步骤,如果没有close文件描述符,操作系统会直接回收资源,并不管你用户态的缓冲区是否有数据没有刷新,即你丢失了这部分数据。

二、版本二:注册析构函数/嵌套垃圾回收

        既然版本一存在这种明显的无法正确析构的问题。而在c库中有一个atexit,它可以向操作系统注册一个函数,该函数仅会在程序正常终止时被调用

(1)使用atexit注册程序结束时的函数

class Singleton
{
public://获取全局访问点static Singleton* GetInstance(){if (_instance == nullptr){_instance = new Singleton();atexit(Destructor);}return _instance;}private:static void Destructor(){if (_instance != nullptr){delete _instance;_instance = nullptr;}}private:static Singleton* _instance;	//全局访问点//私有化各种构造函数,防止外面任意创建对象
private:Singleton() {};		//构造~Singleton() {};	//析构Singleton(const Singleton&) = delete;		//拷贝构造Singleton& operator=(const Singleton&) = delete;		//赋值运算符重载Singleton(Singleton&&) = delete;		//移动构造Singleton& operator=(Singleton&&) = delete;		//移动赋值运算符重载
};
Singleton* Singleton::_instance = nullptr;	//初始化静态成员

(2)使用对象嵌套垃圾回收

        利用GarbageCollector静态全局对象在程序正常结束的时候,会自动调用其析构函数,而在他的析构函数中又调用了单例对象的析构函数,从而完成回收。简单来说就是利用了智能指针RAII的思路。

class Singleton {
private:static Singleton* _instance;// 嵌套垃圾回收类class GarbageCollector {public:~GarbageCollector() {if (Singleton::_instance != nullptr) {delete Singleton::_instance;Singleton::_instance = nullptr;}}};static GarbageCollector _gc; // 全局静态成员,程序结束时自动析构Singleton() {std::cout << "Singleton created" << std::endl;}~Singleton() {std::cout << "Singleton destroyed" << std::endl;}public:static Singleton* GetInstance() {if (_instance == nullptr) {_instance = new Singleton();}return _instance;}// 禁用拷贝和移动操作Singleton(const Singleton&) = delete;Singleton& operator=(const Singleton&) = delete;Singleton(Singleton&&) = delete;Singleton& operator=(Singleton&&) = delete;
};Singleton* Singleton::_instance = nullptr;
Singleton::GarbageCollector Singleton::_gc;

三、版本三:线程安全

        虽然版本二在单线程场景下已经足够使用。但在多线程情况,却会出现重复走到if,然后创建多个对象的竞态问题。

        这个代码中使用到了双重检测机制。即使在没有创建单例对象的时候,多个线程进入了第一个if里面,然后会因为锁竞争只能有一个线程执行到第二个if里面去创建单例对象。当他释放锁后,别的线程会继续竞争锁并判断是否为nullptr。如果为空则退出。

        所以在这个代码中,只会有第一次n个线程进入if后的n次加锁、解锁开销。

// 双重检查锁定(DCL)
class Singleton 
{
private:static Singleton* instance;static std::mutex mtx;Singleton() {}public:static Singleton* getInstance() {if (instance == nullptr) {  // ① 第一次检查(无锁)std::lock_guard<std::mutex> lock(mtx);  // 加锁if (instance == nullptr) {  // ② 第二次检查(有锁)instance = new Singleton();atexit(Destructor);//	向操作系统注册析构函数}}return instance;}private:static void Destructor(){if (instance != nullptr){delete instance;instance = nullptr;}}
private://禁用各种构造Singleton() {};		//构造~Singleton() {};	//析构Singleton(const Singleton&) = delete;		//拷贝构造Singleton& operator=(const Singleton&) = delete;		//赋值运算符重载Singleton(Singleton&&) = delete;		//移动构造Singleton& operator=(Singleton&&) = delete;		//移动赋值运算符重载
};

四、版本四:编译器、CPU指令重排问题

        解决了多线程竞态问题后,发现编译器、CPU会按照单线程的执行思想,自以为是的优化执行顺序,这就导致了new本身可能乱序。

        new操作符在底层会分为三个步骤:

        其中operator new是基于内存池的,所以他是线程安全的。而构造对象这一步是程序员手动执行的,既不线程安全,执行顺序也不能保证。

编译器或 CPU 为了优化性能,可能把步骤 3 调整到步骤 2 之前,变成:

所以我们需要使用内存屏障来保证执行流的可见性问题。

同时由于对普通指针 instance 的读写不是原子操作。在多线程环境下,可能出现线程 A 写入指针的 “中间状态”(比如只更新了低 32 位),线程 B 读取时拿到一个无效的指针值,直接崩溃。所以用原子操作解决原子性问题。

class Singleton 
{
public:static Singleton* getInstance() {Singleton* tmp = instance.load(std::memory_order_acquire);  // 读操作if (tmp == nullptr) {std::lock_guard<std::mutex> lock(mtx);tmp = instance.load(std::memory_order_relaxed);if (tmp == nullptr) {tmp = new Singleton();// 写操作:禁止重排,保证构造完成后再赋值instance.store(tmp, std::memory_order_release);  }}return tmp;}
private:// 用 atomic 修饰指针,禁止指令重排static std::atomic<Singleton*> instance;  static std::mutex mtx;
};
std::atomic<Singleton*> Singleton::instance(nullptr);
std::mutex Singleton::mtx;

关于这里的内存屏障、原子操作只需要大致认识即可,后续会有文章详细讲解。

        虽然这种方式已经足够,但写起来太过繁琐。

五、版本五:局部静态变量线程安全性

        在C++11后规定magic static的特性:

  • 局部静态变量(如 static Singleton instance)的初始化是线程安全的。若多个线程同时首次调用 GetInstance(),编译器会保证只有一个线程执行变量初始化,其他线程会阻塞等待初始化完成后再访问,无需手动加锁。即保证了线程安全性,又保证了可见性问题。
  • 自动销毁:程序结束时,局部静态变量会按构造的逆序自动销毁,调用 ~Singleton() 释放资源。
  • 注意:只有局部静态变量才能这么做,如果是全局静态变量则未被标准保证,仍需使用之前的方式。
#include <iostream>
// 如需线程安全验证,可包含此头文件(C++11及以上环境)
#include <thread>  class Singleton {
public:// 核心:局部静态变量,C++11后保证线程安全初始化//并使用&来保证访问效率    static Singleton& GetInstance() {static Singleton instance;  // 第一次调用时初始化,后续直接返回引用return instance;}// 示例:单例的业务方法void DoSomething() const {std::cout << "Singleton is working, address: " << this << std::endl;}private:// 1. 私有构造:禁止外部直接创建Singleton() {std::cout << "Singleton constructed." << std::endl;}// 2. 私有析构:禁止外部直接销毁(由系统自动调用)~Singleton() {std::cout << "Singleton destructed." << std::endl;}// 3. 禁用拷贝语义:防止对象复制Singleton(const Singleton&) = delete;Singleton& operator=(const Singleton&) = delete;// 4. 禁用移动语义:防止对象移动Singleton(Singleton&&) = delete;Singleton& operator=(Singleton&&) = delete;
};

        这种方式也是我们最推荐的写法,他即不用考虑无法自动析构导致资源泄露的问题,也不用考虑线程安全,最后甚至不需要考虑CPU编译器的指令重排,可以说局部静态变量的标准出现,让单例模式得到了显著的进步。

        但有人说,这样你每写一个单例类就需要手动禁用一下其构造函数等等,还是稍显麻烦,那么我们下面的写法则将他封装成了一个基类。

六、版本六:模板提高复用率

        当父类的各种构造被禁用了,子类想要调用对应的构造,首先会调用父类的,然后发现错误,实现单例模式,且不需要在子类手动禁用。

// 单例模式基类模板
template <typename T>
class Singleton {
public:// 禁用拷贝构造Singleton(const Singleton&) = delete;// 禁用拷贝赋值Singleton& operator=(const Singleton&) = delete;// 禁用移动构造Singleton(Singleton&&) = delete;// 禁用移动赋值Singleton& operator=(Singleton&&) = delete;// 获取单例实例static T& getInstance() {// 静态局部变量,C++11后保证线程安全初始化static T instance;return instance;}protected:// 保护的构造函数,允许子类构造Singleton() = default;// 保护的析构函数,允许子类析构virtual ~Singleton() = default;
};

当你使用的时候,只需要继承于该基类,然后重写其中的构造函数、析构函数即可,举个例子:

// 1. 日志管理器 - 单例应用场景
class Logger : public Singleton<Logger> {friend class Singleton<Logger>;
private:// 私有构造函数,初始化日志系统Logger() {std::cout << "Logger initialized. Starting to log messages..." << std::endl;}// 私有析构函数,清理日志系统~Logger() {std::cout << "Logger shutting down. Finalizing log files..." << std::endl;}public:// 日志级别enum class Level { INFO, WARNING, ERROR };// 记录日志的方法void log(const std::string& message, Level level = Level::INFO) {// 简单的线程安全处理std::lock_guard<std::mutex> lock(mtx);// 根据级别输出不同前缀std::string prefix;switch(level) {case Level::INFO:    prefix = "[INFO]   "; break;case Level::WARNING: prefix = "[WARNING]"; break;case Level::ERROR:   prefix = "[ERROR]  "; break;}// 输出日志信息std::cout << prefix << message << std::endl;}private:std::mutex mtx; // 确保日志输出线程安全
};

        这里可以看到他引入了一个友元类,让基类可以访问到子类的私有构造、析构函数。在之前的设计模式中由于子类重写的函数都是public的,所以不需要友元。这一点需要注意一下。

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

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

相关文章

JAiRouter 0.2.1 更新啦:内存优化 + 配置合并 + IP 限流增强,运维体验再升级

JAiRouter 0.2.1 更新啦&#xff1a;内存优化 配置合并 IP 限流增强&#xff0c;运维体验再升级 如果你已经在 0.2.0 生产环境中稳定运行&#xff0c;那么这篇更新会让你无痛升级&#xff0c;直接“更轻、更稳、更省心”。 &#x1f4ce; 官方仓库 & issue 直达 https://…

学习嵌入式第二十六天

文章目录IO(续上)1.标准IO1.标准IO的接口2.流的定位2.文件IO1.概念&#xff1a;2.系统调用和库函数3.文件IO函数接口习题IO(续上) 1.标准IO 1.标准IO的接口 fwrite 原型&#xff1a;size_t fwrite(const void *ptr, size_t size, size_t nmemb,FILE *stream); 功能&#xff1…

GDB 程序启动参数设置深度指南

GDB 程序启动参数设置深度指南 1. 概述 在程序调试过程中&#xff0c;正确设置启动参数对于验证程序行为、重现特定场景至关重要。GDB提供多种灵活的方式设置启动参数&#xff0c;特别是当您需要调试命令行参数处理逻辑或配置敏感型应用时。 2. 参数设置的核心方法 2.1 启动GDB…

Autudl华为昇腾系列NPU简介和部署推理yolo11 yolov8 yolov5分割模型

0.配置Autudl 下面图片是我所租的昇腾卡和具体环境版本&#xff0c;太具体的就不说了&#xff0c;有需要的话我单独出一期Autudl租显卡的教程&#xff0c;主要是为了学习昇腾环境如何运行Yolo系列模型。 0.1华为昇腾芯片&#xff08;Ascend&#xff09;简介 1.Ascend 310&…

什么是JSP和Servlet以及二者的关系

JSP&#xff08;JavaServer Pages&#xff09; 是“HTML 里写 Java”的模板技术&#xff0c;最终会被容器转换成 Servlet。Servlet 是“Java 里写 HTML”的 Java 类&#xff0c;直接继承 javax.servlet.http.HttpServlet&#xff0c;用来接收/响应 HTTP 请求。Servlet 是什么 纯…

【WonderTrader源码详解 1】【环境搭建 2】【编译安装WonderTrader】

一、引言 本篇来讲述如何搭建 wondertrader 和 wtpy 二、wondertrader 2.1 源码下载 # /home/leo/sda_1.6TBgit clone https://gitee.com/wondertrader/wondertrader.gitgit clone https://gitee.com/wondertrader/wtpy.git2.2 源码编译 cd /home/leo/sda_1.6TB/wondertrader/s…

hutool 作为http 客户端工具调用的一点点总结

场景一&#xff1a;客户端timeout 的时间给的很短//100毫秒 private static final int HTTP_TIMEOUT_MS 1 * 100; response HttpUtil.createPost(patrolresultconfirmUrl).body(JSONObject.toJSONString(search)).header("Authorization", token).timeout(HTTP_TI…

基于MongoDB/HBase的知识共享平台的设计与实现

标题:基于MongoDB/HBase的知识共享平台的设计与实现内容:1.摘要 在当今信息爆炸的时代&#xff0c;知识的有效共享和管理变得愈发重要。本研究的目的是设计并实现一个基于MongoDB/HBase的知识共享平台&#xff0c;以满足大规模知识数据的存储、高效查询和快速共享需求。方法上&…

PHP数组操作:交集、并集和差集

1. 交集&#xff08;Intersection&#xff09;交集是指两个集合中都存在的元素。$array1 [1, 2, 3, 4]; $array2 [3, 4, 5, 6];$intersection array_intersect($array1, $array2); print_r($intersection); // 输出: Array ( [2] > 3 [3] > 4 )2. 并集&#xff08;Uni…

Qt 常用控件 - 7

Text Edit&#xff08;多行输入框&#xff09;QTextEdit 表示多行输入框&#xff0c;也是一个富文本 & markdown 编辑器&#xff0c;能在内容超出范围时自动提供滚动条。QTextEdit&#xff1a;不仅仅能表示纯文本&#xff0c;还可以表示 htnl 和 markdownQPlainTextEdit&am…

JDK、eclipse的安装,配置JDK、Tomcat并使用eclipse创建项目

目录一、JDK的安装1. 安装JDK2. 配置环境变量3. 检查jdk是否已安装二、eclipse的安装1. 解压安装2. 设置字体大小3. 设置拼写提示三、tomcat安装四、创建项目1. 第一次创建一个普通的java项目2. 第一次创建一个java web项目扩展&#xff1a;运行项目报Tomcat端口占用&#xff0…

Iptables 详细使用指南

目录 1. 工作原理​ 2. 核心架构&#xff08;四表五链&#xff09; 2.1 四张表&#xff08;优先级从高到低&#xff09; 2.2 五条内置链&#xff08;数据包流向&#xff09; 3. Iptables规则 3.1 规则的匹配条件与目标动作 常见匹配条件&#xff08;用于筛选数据包&…

Vue 服务端渲染(SSR)详解

Vue SSR是一种在服务端将 Vue 应用渲染成 HTML 字符串&#xff0c;然后直接发送到客户端的技术。相比传统的客户端渲染&#xff0c;Vue SSR 能带来更好的 SEO 性能和更快的首屏加载时间。下面我们从零到一&#xff0c;结合项目源码&#xff0c;详细讲解如何实现一个 Vue SSR 项…

机器翻译:需要了解的数学基础详解

文章目录一、概率论与统计学1.1 基本概念1.2 在机器翻译中的应用二、线性代数2.1 基本概念2.2 在机器翻译中的应用三、微积分3.1 基本概念3.2 在机器翻译中的应用四、信息论4.1 基本概念4.2 在机器翻译中的应用五、数值优化5.1 优化问题形式化5.2 优化算法5.3 正则化技术六、图…

蓝桥杯手算题和杂题简易做法

一、巧用Excel Excel在解决某些数学问题时非常高效&#xff0c;特别是涉及表格计算、简单统计和可视化分析时。 门牌制作 这道题是一道基础题&#xff0c;只需要判断每个数字有几个2&#xff0c;然后在加起来即可&#xff0c;但是还有更简单的方法&#xff0c;先通过编译器&…

5. 缓存-Redis

文章目录前言一、 介绍1. 简介2. 核心特点二、 应用场景1. 应用场景2. 数据类型作用场景三、 性能特性1. 内存2. 高性能数据结构3. 单线程、多路复用四、 异步持久化机制1. RDB&#xff08;Redis Database&#xff09;2. AOF&#xff08;Append-Only File&#xff09;3. 持久化…

如何理解Tomcat、Servlet、Catanalina的关系

目录 背景&#xff1a; 结论&#xff1a; 好文-【拓展阅读】&#xff1a; 象漂亮更新动力&#xff01; 背景&#xff1a; 学习Java的Servlet时&#xff0c;常常说Tomcat是一个容器&#xff0c;我们写ServletA,ServletB,Tomcat容器在启动的时候会读取web.xml或者我们程序中的…

Hive的并行度的优化

对于分布式任务来说&#xff0c;任务执行的并行度十分重要。Hive的底层是MapReduce&#xff0c;所以Hive的并行度优化分为Map端优化和Reduce端优化。(1)、Map端优化Map端的并行度与Map切片数量相关&#xff0c;并行度等于切片数量。一般情况下不用去设置Map端的并行度。以下特殊…

Vue.js 响应接口:深度解析与实践指南

Vue.js 响应接口&#xff1a;深度解析与实践指南 引言 随着前端技术的不断发展&#xff0c;Vue.js 作为一种流行的前端框架&#xff0c;已经成为了众多开发者的首选。Vue.js 的响应式系统是其核心特性之一&#xff0c;它允许开发者轻松实现数据的双向绑定。而响应接口则是Vue.j…

高精度蓝牙定位:技术、应用与未来发展

一、高精度蓝牙定位概述在当今科技飞速发展的时代&#xff0c;定位技术的精度和可靠性变得越来越重要。高精度蓝牙定位作为一种新兴的定位技术&#xff0c;正逐渐崭露头角。蓝牙技术是一种支持设备短距离通信&#xff08;一般10m内&#xff09;的无线电技术&#xff0c;能在包括…