当前位置: 首页 > news >正文

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

在这里插入图片描述

文章目录

    • 一、引言
    • 二、饿汉模式 (Eager Initialization)
      • 1、实现代码与剖析
      • 2、接口规范详解
      • 3、生命周期与资源管理
      • 4、优缺点权衡
    • 三、懒汉模式 (Lazy Initialization)
      • 1、线程不安全的实现及风险
        • 竞态条件(Race Condition)的深入分析:
    • 四、线程安全的懒汉模式:演进之路
      • 1、方案一:粗粒度加锁 (Coarse-Grained Locking)
      • 2、方案二:双重检查锁定模式 (DCLP)
        • DCLP的陷阱与现代C++的解法
      • 3、方案三:C++11 静态局部变量 (Meyers' Singleton) - 终极方案
        • 实现代码
        • 深度解析
    • 五、单例模式的批判性思考
      • 替代方案
    • 六、总结


如果觉得本文对您有所帮助,点个赞和关注吧,谢谢!!!你的支持就是我持续更新的最大动力


一、引言

单例模式(Singleton Pattern),作为GoF(Gang of Four)23种设计模式之一,是软件工程中认知度最高的创建型模式。其核心宗旨在于限制一个类的实例化过程,确保在整个应用程序的生命周期中,该类只存在一个实例,并提供一个全局统一的访问点来获取此实例。

本文将探讨C++中单例模式的各种实现范式,从经典的饿汉模式懒汉模式出发,重点阐述在现代C++多线程环境下,如何从粗粒度加锁双重检查锁定(DCLP),最终到被誉为最佳实践的 C++11 Meyers’ Singleton


二、饿汉模式 (Eager Initialization)

饿汉模式的哲学是“空间换时间”,它选择在程序启动阶段就完成实例化,以确保在运行时能无延迟、无线程安全顾虑地获取实例。

1、实现代码与剖析

#include <iostream>class EagerSingleton {
public:/*** @brief 获取单例实例的全局访问点。* @details 此函数是线程安全的,因为它仅返回一个在程序启动时*          就已经被初始化的静态成员变量。* @return EagerSingleton& - 对唯一实例的常量左值引用。*         返回引用可以防止调用者意外删除实例,并提供更自然的成员访问语法。*/static EagerSingleton& getInstance() {return instance;}// [规则] 禁止拷贝构造与赋值,以维护实例的唯一性。// 在C++11及以后版本,使用=delete明确地禁用这些函数是最佳实践。EagerSingleton(const EagerSingleton&) = delete;EagerSingleton& operator=(const EagerSingleton&) = delete;void someBusinessLogic() {std::cout << "EagerSingleton is performing some business logic." << std::endl;std::cout << "Instance address: " << this << std::endl;}private:/*** @brief [规则] 私有化构造函数。* @details 这是实现单例模式的基石。通过将构造函数设为私有,*          我们阻止了任何外部代码通过 `new EagerSingleton()` 或*          在栈上创建 `EagerSingleton obj;` 的企图,*          从而将实例化的唯一控制权收归类自身。*/EagerSingleton() {std::cout << "EagerSingleton instance has been created at program startup." << std::endl;}/*** @brief [核心] 静态成员实例。* @details `static` 关键字确保 `instance` 对象在类的所有实例间共享,*          且在程序的整个生命周期中只有一个副本。其初始化发生在*          main函数执行之前,属于静态初始化阶段。*/static EagerSingleton instance;
};// 在类定义之外,全局命名空间中对静态成员进行定义和初始化。
// 这是C++语法要求的。
EagerSingleton EagerSingleton::instance;// --- 使用示例 ---
int main() {EagerSingleton::getInstance().someBusinessLogic();EagerSingleton& s2 = EagerSingleton::getInstance();s2.someBusinessLogic(); // s2与第一次调用获取的是同一个实例return 0;
}

2、接口规范详解

  • 函数作用getInstance() 是外界获取EagerSingleton唯一实例的唯一合法入口。
  • 使用格式模版ClassName::getInstance()
  • 参数含义:无参数。
  • 返回值EagerSingleton&
    • 类型:返回一个左值引用 (&)。
    • 优势
      1. 安全性:调用者无法对引用执行 delete 操作,避免了实例被错误释放。
      2. 非空保证:引用不能为 null,调用者无需进行空指针检查。
      3. 语法便利:可以直接使用 . 操作符访问成员,如 EagerSingleton::getInstance().someBusinessLogic();,而非指针的 ->

3、生命周期与资源管理

  • 创建时机EagerSingleton::instance 具有静态存储期。它的构造函数在 main 函数执行前的静态初始化阶段被调用。
  • 销毁时机:其析构函数将在程序正常退出时(例如 main 函数返回或调用 exit())自动被调用。这意味着如果 EagerSingleton 的析构函数需要释放资源(如关闭文件、断开网络连接),这一过程是自动且确定的。
  • 线程安全性:由于实例化发生在任何线程创建之前,因此完全不存在多线程竞争创建实例的问题,是天生线程安全的。

4、优缺点权衡

  • 优点

    • 实现简单:逻辑清晰,代码量少。
    • 无锁线程安全:是所有实现中最直接的线程安全方案。
  • 缺点

    • 资源预占:即使整个程序运行期间一次都未使用该单例,其资源(内存、构造函数中的操作)也被占用和执行,造成潜在浪费。
    • 启动延迟:若单例的构造函数非常耗时(如加载大型配置文件、建立网络连接),会明显拖慢程序的启动速度。
    • 静态初始化顺序灾难 (Static Initialization Order Fiasco):如果多个全局静态对象(包括单例)的初始化存在依赖关系,C++标准不保证它们之间的初始化顺序。一个单例可能在构造时试图使用另一个尚未初始化的单例,导致未定义行为。

三、懒汉模式 (Lazy Initialization)

懒汉模式的哲学是“延迟加载”,实例只在第一次被请求时才创建。这避免了饿汉模式的资源预占问题,但也引入了线程安全的挑战。

1、线程不安全的实现及风险

这是懒汉模式最朴素的实现,严禁在多线程环境中使用

class UnsafeLazySingleton {
public:static UnsafeLazySingleton* getInstance() {// [风险点] 检查与创建非原子操作if (instance == nullptr) {instance = new UnsafeLazySingleton();}return instance;}// ...
private:UnsafeLazySingleton() { /* ... */ }static UnsafeLazySingleton* instance;
};
UnsafeLazySingleton* UnsafeLazySingleton::instance = nullptr;
竞态条件(Race Condition)的深入分析:

设想两个线程(Thread A, Thread B)并发执行 getInstance()

  1. T1: Thread A 执行 if (instance == nullptr),判断为 true
  2. T2: CPU上下文切换,Thread A被挂起。
  3. T3: Thread B 开始执行 getInstance(),它也执行 if (instance == nullptr),判断同样为 true
  4. T4: Thread B 执行 instance = new UnsafeLazySingleton();,成功创建了一个实例。
  5. T5: CPU上下文切换,Thread B被挂起,Thread A恢复执行。
  6. T6: Thread A 从 if 语句后继续,执行 instance = new UnsafeLazySingleton();创建了第二个实例

后果:单例的唯一性被破坏,且第一个由Thread B创建的实例的地址被覆盖,导致内存泄漏


四、线程安全的懒汉模式:演进之路

1、方案一:粗粒度加锁 (Coarse-Grained Locking)

使用互斥锁(Mutex)是解决竞态条件最直接的手段。

#include <mutex>class CoarseLockLazySingleton {
public:static CoarseLockLazySingleton* getInstance() {// RAII技法,确保锁在任何情况下都能被释放std::lock_guard<std::mutex> lock(mtx);if (instance == nullptr) {instance = new CoarseLockLazySingleton();}return instance;}// ...
private:CoarseLockLazySingleton() {}static CoarseLockLazySingleton* instance;static std::mutex mtx;
};CoarseLockLazySingleton* CoarseLockLazySingleton::instance = nullptr;
std::mutex CoarseLockLazySingleton::mtx;
  • std::mutex:一个互斥锁对象,用于保护临界区。
  • std::lock_guard:一个RAII(资源获取即初始化)封装类。在其构造函数中锁定传入的 mutex,在其析构函数(即对象离开作用域时)中自动解锁。这极大地简化了锁的管理,避免了因忘记解锁或异常抛出导致的死锁。
  • 性能瓶颈:此方案虽然安全,但效率低下。在实例被创建之后,每一次对 getInstance() 的调用仍然需要获取和释放锁,这是不必要的性能开销,尤其是在高并发场景下。

2、方案二:双重检查锁定模式 (DCLP)

DCLP旨在优化上述性能问题,其核心思想是:仅在实例指针为 nullptr 时才进入同步块。

#include <atomic>
#include <mutex>class DCLPSingleton {
public:static DCLPSingleton* getInstance() {// 第一次检查 (无锁): 绝大多数调用在此处直接返回,避免锁开销if (instance.load(std::memory_order_acquire) == nullptr) {std::lock_guard<std::mutex> lock(mtx);// 第二次检查 (有锁): 防止在等待锁期间其他线程已创建实例if (instance.load(std::memory_order_relaxed) == nullptr) {instance.store(new DCLPSingleton(), std::memory_order_release);}}return instance.load(std::memory_order_relaxed);}// ...
private:DCLPSingleton() {}// [关键] 使用 std::atomic 保证可见性和禁止指令重排static std::atomic<DCLPSingleton*> instance;static std::mutex mtx;
};std::atomic<DCLPSingleton*> DCLPSingleton::instance{nullptr};
std::mutex DCLPSingleton::mtx;
DCLP的陷阱与现代C++的解法

在C++11之前,DCLP是不可靠的。原因是 指令重排 (Instruction Reordering)new DCLPSingleton() 并非原子操作,它包含三个步骤:

  1. operator new:分配内存。
  2. DCLPSingleton::DCLPSingleton():在分配的内存上调用构造函数。
  3. assignment:将分配的内存地址赋值给 instance 指针。

编译器和CPU为了优化,可能会将步骤3重排到步骤2之前。此时,若发生线程切换,另一线程在第一次检查时会看到 instancenullptr,便直接返回一个尚未构造完成的对象,对其访问将导致未定义行为。

现代C++的解决方案:std::atomic 与内存序

  • std::atomic<T*>:它保证了对指针 instance 的读写操作是原子的,不会被其他线程看到中间状态。更重要的是,它提供了内存屏障,以控制内存操作的顺序。
  • std::memory_order
    • instance.load(std::memory_order_acquire)Acquire语义。确保在此load操作之后的任何读写操作,都不会被重排到此load之前。它保证了如果读到了非 nullptr 的值,那么写入该值的线程中所有在该写入操作之前的写入,对当前线程都可见(即构造函数已完成)。
    • instance.store(..., std::memory_order_release)Release语义。确保在此store操作之前的任何读写操作,都不会被重排到此store之后。它保证了构造函数的所有操作都已完成,才将新地址写入 instance,并使这些写入对其他看到此store结果的线程可见。
    • std::memory_order_relaxed:只保证原子性,不提供任何顺序保证。用在已经确定实例已创建的情况下,性能最高。

DCLP虽然在现代C++中可以被正确实现,但其复杂性高,易于出错,通常不作为首选。

3、方案三:C++11 静态局部变量 (Meyers’ Singleton) - 终极方案

C++11标准为我们带来了最简洁、最优雅、最安全的懒汉单例实现。

这意味着,函数内的静态局部变量的初始化,由语言标准保证是线程安全的。

实现代码
#include <iostream>class MeyersSingleton {
public:/*** @brief 获取单例实例的全局访问点* @details C++11及以后标准保证函数内部的静态局部变量的初始化*          是线程安全的,且只执行一次。* @return MeyersSingleton& - 对唯一实例的引用。*/static MeyersSingleton& getInstance() {static MeyersSingleton instance; // 魔法发生于此return instance;}MeyersSingleton(const MeyersSingleton&) = delete;MeyersSingleton& operator=(const MeyersSingleton&) = delete;void someBusinessLogic() {std::cout << "Meyers' Singleton is performing logic." << std::endl;std::cout << "Instance address: " << this << std::endl;}private:MeyersSingleton() {std::cout << "Meyers' Singleton instance has been created on first use." << std::endl;}
};int main() {MeyersSingleton::getInstance().someBusinessLogic();return 0;
}
深度解析
  • 实现原理:当 getInstance() 第一次被调用时,程序会执行到 static MeyersSingleton instance;。此时,会进行 instance 的构造。编译器会自动生成一段代码(通常包含一个布尔标志和一把锁),以确保即使多个线程同时首次进入 getInstance(),构造函数也只会被执行一次。后续的调用会直接跳过初始化,返回已存在的 instance
  • 生命周期:与饿汉模式类似,instance 同样具有静态存储期。它的析构函数会在程序结束时被自动调用。std::atexit 或类似机制会注册其销毁函数。
  • 性能:第一次调用时有初始化的开销(可能包含一次锁同步),但后续所有调用都没有任何锁开销,几乎等同于返回一个普通静态变量,性能极高。

五、单例模式的批判性思考

尽管单例模式被广泛使用,但它也常常被视为一种“反模式”(Anti-Pattern),因为它存在一些固有的设计缺陷。

  • 全局状态的危害:单例本质上是伪装成对象的全局变量。全局状态使得代码的依赖关系变得隐晦,难以追踪和推理,增加了系统的复杂性和耦合度。
  • 可测试性难题:依赖于单例的类很难进行单元测试。因为无法轻易地用一个模拟(Mock)对象来替换单例实例,导致测试必须在单例的真实环境下进行,违背了单元测试的隔离性原则。
  • 违反单一职责原则 (SRP):一个类除了承担其核心业务职责外,还承担了管理自身实例数量和生命周期的职责。
  • 并发下的销毁问题:如果单例的析构函数中有复杂逻辑,而在程序退出时仍有其他线程在使用该单例,可能会引发数据竞争或崩溃。

替代方案

在许多场景下,依赖注入 (Dependency Injection, DI) 是一个更优秀的选择。通过将依赖(如日志记录器、配置管理器)作为构造函数参数或Setter方法传入,而不是让类自己去全局获取,可以极大地提高代码的模块化、可测试性和灵活性。


六、总结

实现范式线程安全懒加载实现复杂度性能开销推荐度
饿汉模式启动时开销,运行时无开销⭐⭐⭐⭐
懒汉 (加锁)⭐⭐每次调用都有锁开销⭐⭐
懒汉 (DCLP)⭐⭐⭐⭐⭐复杂,但初始化后无锁开销⭐⭐⭐
懒汉 (Meyers’)初始化后无锁开销⭐⭐⭐⭐⭐

在任何支持C++11及以上标准的现代C++项目中,Meyers’ Singleton (基于静态局部变量的实现) 是实现懒汉式单例无可争议的最佳选择。 它完美地结合了代码的简洁性、线程安全性、懒加载特性以及卓越的性能。

只有在明确需要程序启动时即完成初始化,且不关心其带来的启动延迟和资源预占问题时,饿汉模式才是一个值得考虑的备选方案。请始终审慎使用单例模式,并优先考虑依赖注入等更灵活的架构设计。

如果觉得本文对您有所帮助,点个赞和关注吧,谢谢!!!你的支持就是我持续更新的最大动力


文章转载自:

http://RmeINZNI.xcdph.cn
http://JjJcgmL5.xcdph.cn
http://cTYfey6h.xcdph.cn
http://cZ9cgUNo.xcdph.cn
http://RwYuaqDU.xcdph.cn
http://ZloYOWDI.xcdph.cn
http://RYrhWZ2v.xcdph.cn
http://8mO4c4s4.xcdph.cn
http://kzo2fDfA.xcdph.cn
http://Vz6rEGyW.xcdph.cn
http://ofUYofs5.xcdph.cn
http://V1VwMTQd.xcdph.cn
http://tz5ByfJz.xcdph.cn
http://YNu30WAU.xcdph.cn
http://7ozCNWw0.xcdph.cn
http://yqUBnWrX.xcdph.cn
http://U32QZaSC.xcdph.cn
http://RkhmppDl.xcdph.cn
http://9j1zhSXX.xcdph.cn
http://A5qgP3bp.xcdph.cn
http://LQVH7vS9.xcdph.cn
http://vOmpHMpF.xcdph.cn
http://06i5BjBO.xcdph.cn
http://1rL8Dvf9.xcdph.cn
http://9ihJveiM.xcdph.cn
http://mfYROMnE.xcdph.cn
http://sfGGfatX.xcdph.cn
http://y5JOoriw.xcdph.cn
http://3dsWdUiL.xcdph.cn
http://cS7oFP4A.xcdph.cn
http://www.dtcms.com/a/374885.html

相关文章:

  • C# ---ToLookUp
  • CSS in JS 的演进:Styled Components, Emotion 等的对比与选择
  • mybatis-plus多租户兼容多字段租户标识
  • Flutter跨平台工程实践与原理透视:从渲染引擎到高质产物
  • 华为云盘同步、备份和自动上传功能三者如何区分
  • 设计模式第一章(建造者模式)
  • Vue3入门到实战,最新版vue3+TypeScript前端开发教程,笔记02
  • 【Vue】Vue2 与 Vue3 内置组件对比
  • XSS 跨站脚本攻击剖析与防御 - 第一章:XSS 初探
  • vue 去掉el-dropdown 悬浮时出现的边框
  • 常见的排序算法总结
  • [优化算法]神经网络结构搜索(一)
  • php 使用html 生成pdf word wkhtmltopdf 系列2
  • 大数据毕业设计选题推荐-基于大数据的海洋塑料污染数据分析与可视化系统-Hadoop-Spark-数据可视化-BigData
  • 【计算机网络 | 第11篇】宽带接入技术及其发展历程
  • 探索Java并发编程--从基础到高级实践技巧
  • Made in Green环保健康产品认证怎么做?
  • yum list 和 repoquery的区别
  • 解决HTML/JS开发中的常见问题与实用资源
  • Angular 面试题及详细答案
  • AI与AR融合:重塑石化与能源巡检的未来
  • 增强现实光学系统_FDTD_zemax_speos_学习(1)
  • 开学季干货——知识梳理与经验分享
  • Alex Codes团队并入OpenAI Codex:苹果生态或迎来AI编程新篇章
  • The learning process of Decision Tree Model|决策树模型学习过程
  • 六、与学习相关的技巧(下)
  • 《低功耗音频:重塑听觉体验与物联网边界的蓝牙革命》
  • 20250909的学习笔记
  • 金融量化指标--5Sortino索提诺比率
  • 消息三剑客华山论剑:Kafka vs RabbitMQ vs RocketMQ