C++:(5) 单例模式与支持初始化失败的单例模式

1. 单例模式基础

禁用拷贝和移动共四个构造函数。

1.1 饿汉式(线程安全)

1. 代码实现

cpp 复制代码
class Singleton {
private:
    static Singleton* instance;
    Singleton() {}
public:
    static Singleton* getInstance() {
        return instance;
    }
};
// 在类外立即初始化
Singleton* Singleton::instance = new Singleton();

2. 实现原理

  • 初始化时机 :在程序进入 main() 函数之前,由 C++ 运行时系统或动态链接器完成静态对象的初始化。
  • 线程安全机制:依赖于操作系统的加载机制。在单线程的初始化阶段完成实例创建,因此天然线程安全,无需加锁。

3. 优点

  • 实现简单:代码量少,逻辑直观。
  • 天然线程安全:不存在多线程竞争问题。
  • 访问速度快:getInstance 只是返回指针,无判断、无锁开销。

4. 缺点

  • 资源浪费:无论程序是否使用该单例,它都会在启动时被创建。如果单例占用大量内存或包含耗时操作(如连接数据库),会拖慢启动速度。
  • 静态初始化顺序问题 :如果该单例在构造函数中依赖了其他全局变量/静态对象 ,而那些对象尚未初始化,会导致未定义行为(Crash)。
    • 场景:Singleton 依赖 GlobalConfig,但链接器可能先初始化 Singleton。
  • 对象在堆上分配,不会自动析构。
  • 无法控制析构顺序:程序退出时,静态对象的析构顺序是不确定的。如果其他静态对象在析构时使用了该单例,而单例已经析构,会导致 Segfault。

5. 适用场景

  • 单例对象轻量,且肯定会被使用。
  • 不依赖其他全局状态。

1.2 懒汉式(线程不安全)

1. 代码实现

cpp 复制代码
class Singleton {
private:
    static Singleton* instance;
    Singleton() {}
public:
    static Singleton* getInstance() {
        if (instance == nullptr) {  // 第一次检查
            instance = new Singleton(); // 创建实例
        }
        return instance;
    }
};
Singleton* Singleton::instance = nullptr;

2. 实现原理

  • 初始化时机:第一次调用 getInstance 时。
  • 线程安全机制。完全依赖程序员的调用习惯。

3. 优点

  • 延迟加载:只有真正使用时才创建,节省启动资源。
  • 避免初始化顺序问题:因为是在函数内部调用,此时全局环境通常已就绪。

4. 缺点

  • 线程不安全(严重)
    • 竞态条件:线程 A 和线程 B 同时调用 getInstance。
    • A 检查 instance == nullptr (True)。
    • B 检查 instance == nullptr (True)。
    • A 创建对象,赋值给 instance。
    • B 创建对象,赋值给 instance(覆盖 A 的指针,导致内存泄漏)。
    • 或者两个线程同时 new,导致堆损坏。
  • 性能隐患:即使加锁(见下文 DCL),每次调用都可能需要检查。

5. 适用场景

  • 仅适用于单线程环境 。在 Linux 多线程服务器程序中严禁直接使用

1.3 双重检查锁定 DCL(线程安全)

1. 代码实现

cpp 复制代码
#include <atomic>
#include <mutex>

class Singleton {
private:
    static std::atomic<Singleton*> instance;
    static std::mutex mtx;
    Singleton() {}
public:
    static Singleton* getInstance() {
        Singleton* tmp = instance.load(std::memory_order_acquire); // 1. 第一次检查
        if (tmp == nullptr) {
            std::lock_guard<std::mutex> lock(mtx); // 加锁
            tmp = instance.load(std::memory_order_relaxed); // 2. 第二次检查
            if (tmp == nullptr) {
                tmp = new Singleton();
                instance.store(tmp, std::memory_order_release); // 存储
            }
        }
        return tmp;
    }
};
std::atomic<Singleton*> Singleton::instance{nullptr};
std::mutex Singleton::mtx;

2. 实现原理

  • 核心思想:只在实例为空时加锁,实例创建后直接返回,减少锁竞争。
  • 两次检查
    1. 第一次检查:避免不必要的加锁(性能优化)。
    2. 第二次检查:防止多个线程同时通过第一次检查后重复创建。
  • 内存序
    • 必须使用 std::atomic 配合 memory_order_acquire/release,防止指令重排

3. 防止指令重排

new Singleton() 在底层汇编大致分为三步:

  1. malloc:分配内存。
  2. ctor:调用构造函数初始化内存。
  3. assign:将内存地址赋值给 instance 指针。

编译器/CPU 优化风险: 为了性能,步骤 2 和 3 可能重排为:1 -> 3 -> 2。

  • 后果 :线程 A 执行了 1 和 3,此时 instance 非空,但对象尚未构造完成。线程 B 调用 getInstance,发现 instance 非空,直接返回并使用,导致访问未初始化的内存,程序 Crash。
  • 解决:memory_order_release 确保存储操作前的所有写操作(构造)对其他线程可见;memory_order_acquire 确保加载操作后的读操作能看到之前的写操作。

4. 优点

  • 线程安全:通过锁和原子操作保证。
  • 延迟加载:按需创建。
  • 高性能:实例创建后,后续调用无锁开销。

5. 缺点

  • 对象在堆上分配,不会自动析构(和饿汉式一样)。
  • 性能开销,每次调用都要做一次原子 load

1.4 Meyers(推荐)

1. 代码实现

cpp 复制代码
class Singleton {
public:
    static Singleton& getInstance() {
        static Singleton instance; // 魔法在这里
        return instance;
    }
private:
    Singleton() {}
    ~Singleton() {}
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
};

2. 实现原理

  • 标准保证静态局部变量的初始化是线程安全的
  • 底层机制
    • 编译器会为 static Singleton instance 生成一个隐藏的守卫变量
    • 在 Linux (GCC/Clang) 下,通常通过调用 __cxa_guard_acquire 和 __cxa_guard_release 实现。
    • 底层封装了 pthread_once 或原子锁机制,确保初始化代码块只执行一次。
  • 生命周期
    • 创建:第一次控制流经过声明处时。
    • 销毁:程序退出(main 返回或 exit 调用)时,按反向顺序析构。

3. 优点

  • 线程安全:由语言标准和编译器保证,无需手动加锁。
  • 延迟加载:第一次使用时才初始化。
  • 自动内存管理:栈对象或静态存储区对象,无需 delete,无内存泄漏。
  • 避免静态初始化顺序问题:因为是函数内局部静态,只有在函数被调用时才初始化,此时全局环境已就绪。

4. 缺点

  • 析构顺序风险:如果其他静态对象的析构函数依赖此单例,且此单例先析构,会 Crash。(这是所有全局/静态单例都存在的问题,不仅仅是 Meyers)。
  • 首次调用开销:第一次调用时有锁开销(但通常可忽略)。

5. 适用场景

  • 现代 C++ 开发的首选方案

2. 支持初始化失败的单例模式

2.1 DCL

cpp 复制代码
#pragma once
#include <atomic> 
#include <mutex>   
#include <iostream>  
#include <exception> 
#include <cstdlib>     // std::atexit 注册程序退出时的清理函数

/**
 * @brief 支持初始化失败的双重检查锁单例模式
 * 
 * 核心特性:
 * 1. 线程安全:使用 atomic + mutex + 内存序保证多线程环境下的正确性
 * 2. 懒加载:首次调用 GetInstance() 时才创建实例
 * 3. 高性能:双重检查避免每次获取实例都加锁
 * 4. 初始化容错:Init() 失败时抛出异常,外部可感知创建失败
 * 5. 自动清理:通过 std::atexit 注册析构,避免内存泄漏
 */
class Singleton {
public:
    /**
     * @brief 获取单例实例(线程安全,支持初始化失败)
     * @return Singleton* 成功返回实例指针,失败返回 nullptr
     * 
     * 实现:双重检查锁模式(Double-Checked Locking Pattern, DCLP)
     */
    static Singleton* GetInstance() {
        // ========== 第一次检查:无锁快速路径 ==========
        // 使用 memory_order_acquire 语义:
        // - 保证后续对实例成员的访问不会重排到本次读取之前
        // - 与 store 时的 release 语义配对,形成 "happens-before" 关系
        Singleton* inst = instance_.load(std::memory_order_acquire);
        
        // 如果实例已存在,直接返回(99% 的情况走这个分支,性能极高)
        if (inst == nullptr) {
            // ========== 加锁进入慢速路径 ==========
            // lock_guard 保证作用域结束时自动释放锁,异常安全
            std::lock_guard<std::mutex> lock(mutex_);
            
            // ========== 第二次检查:防止重复创建 ==========
            // 此时已持锁,其他线程不可能同时创建实例
            // 使用 relaxed 即可,因为锁已经保证了同步
            inst = instance_.load(std::memory_order_relaxed);
            
            if (inst == nullptr) {
                try {
                    // ========== 创建实例 ==========
                    // new 操作分两步:1. 分配内存 2. 调用构造函数
                    // 构造函数中会调用 Init(),可能抛出异常
                    inst = new Singleton();
                    
                    // ========== 发布实例(关键)==========
                    // 使用 memory_order_release 语义:
                    // - 保证 new Singleton() 的所有写操作(包括 Init() 的副作用)
                    //   在 store 之前完成,防止指令重排导致其他线程看到"半初始化"的对象
                    // - 与上方 load(acquire) 配对,实现线程间同步
                    instance_.store(inst, std::memory_order_release);
                    
                    // ========== 注册自动清理 ==========
                    // atexit 保证程序正常退出时调用 Destroy() 释放资源
                    // 注意:只注册一次即可,重复注册标准库会去重或忽略
                    std::atexit(Destroy);
                    
                } catch (const std::exception& e) {
                    // ========== 初始化失败处理 ==========
                    // 1. 构造函数/Init() 抛出异常时,new 表达式会自动释放已分配内存
                    //    (C++ 保证:构造函数异常时,operator delete 会被调用)
                    // 2. instance_ 仍为 nullptr,下次调用会重试创建
                    // 3. 实际项目中应记录日志:LOG_ERROR("Singleton init failed: {}", e.what());
                    return nullptr;  // 向调用者传达"创建失败"
                }
            }
        }
        return inst;
    }

    /**
     * @brief 单例的业务方法示例
     */
    void dosomething() {
        std::cout << "Singleton doing something." << std::endl;
    }

private:
    // ========== 静态成员变量(C++17 inline static 可直接在类内定义)==========
    
    // 原子指针:存储单例实例
    // - 用 atomic 避免多线程同时读写指针导致数据竞争
    // - 指针类型 支持懒加载和延迟初始化
    static inline std::atomic<Singleton*> instance_ {nullptr};
    
    // 互斥锁:保护实例创建过程的互斥访问
    // - 只在实例不存在且需要创建时加锁,锁竞争概率极低
    static inline std::mutex mutex_;

    /**
     * @brief 模拟初始化逻辑(可能失败)
     * @return bool true=成功,false=失败
     * 
     * 实际场景示例:
     * - 加载配置文件失败
     * - 连接数据库/网络资源超时
     * - 权限校验不通过
     * - 依赖组件初始化异常
     */
    bool Init() {
        // 示例:模拟 10% 概率初始化失败
        // if (rand() % 10 == 0) return false;
        
        // 实际项目中应实现具体初始化逻辑
        return true;  // 默认成功
    }

    /**
     * @brief 私有构造函数:防止外部直接创建实例
     * @throw std::runtime_error 当 Init() 失败时抛出
     */
    Singleton() {
        // 在构造函数中执行初始化,失败则抛出异常
        // 异常会传播到 GetInstance() 的 try-catch 块中被捕获
        if (!Init()) {
            // 抛出异常前,new 分配的内存会自动释放(C++ 语言保证)
            throw std::runtime_error("Singleton initialization failed");
        }
    }

    /**
     * @brief 静态销毁函数:注册给 std::atexit
     * 
     * 注意:
     * 1. 使用 exchange(nullptr) 原子地将指针置空并获取旧值
     *    - 防止多个 atexit 处理器或手动调用时重复 delete
     *    - 避免悬空指针和 double-free 问题
     * 2. delete 会先调用析构函数,再释放内存
     */
    static void Destroy() {
        Singleton* inst = instance_.exchange(nullptr, std::memory_order_acq_rel);
        delete inst;  // 如果 inst 为 nullptr,delete 是安全的(无操作)
    }

    /**
     * @brief 析构函数:资源清理
     */
    ~Singleton() {
        // 实际项目中应在此释放持有的资源:文件句柄、网络连接、线程池等
        std::cout << "Singleton destructed." << std::endl;
    }
    
    // ========== 禁用拷贝语义(单例核心要求)==========
    Singleton(const Singleton&) = delete;              // 禁止拷贝构造
    Singleton& operator=(const Singleton&) = delete;   // 禁止拷贝赋值
};

2.2 Meyers

cpp 复制代码
#pragma once
#include <iostream>
#include <exception>
#include <atomic>

/**
 * @brief Meyer's Singleton 实现
 * 
 * 核心原理:利用 C++11 局部静态变量的线程安全初始化特性
 * 
 * 优势:
 * 1. 代码极简,无需手动管理 atomic/mutex
 * 2. 编译器保证:局部静态变量初始化是线程安全的(magic statics)
 * 3. 异常安全:构造函数抛异常时,下次调用会重试初始化(C++11 标准保证)
 * 4. 自动析构:程序退出时按反向顺序自动销毁,无需手动注册 atexit
 * 
 * 注意:
 * - 首次初始化失败后,后续调用会重试(符合"可恢复失败"场景)
 * - 如果初始化是"永久失败"(如配置错误),需额外逻辑避免无限重试
 */
class Singleton {
public:
    /**
     * @brief 获取单例实例(线程安全,支持初始化失败重试)
     * @return Singleton* 成功返回实例指针,失败返回 nullptr
     * 
     * C++11 关键保证:
     * - 如果 Instance() 的局部静态变量初始化抛出异常,
     *   则下次调用 GetInstance() 时会重新尝试初始化
     * - 同一时刻只有一个线程执行初始化,其他线程阻塞等待
     */
    static Singleton* GetInstance() {
        try {
            // C++11 起,编译器保证:
            // 1. 多线程环境下,初始化只执行一次(隐式使用 std::call_once)
            // 2. 如果初始化抛异常,标记为"未完成",下次调用重试
            // 3. 初始化完成后,后续调用直接返回,零开销
            static Singleton instance;  // ← 线程安全 + 懒加载 + 自动析构
            
            return &instance;  // 返回栈上对象的地址(实际在静态存储区)
            
        } catch (const std::exception& e) {
            // LOG_ERROR("Singleton initialization failed: {}", e.what());
            return nullptr;  // 向调用者传达"当前不可用"
        }
    }

    /**
     * @brief 单例的业务方法示例
     */
    void dosomething() {
        std::cout << "Singleton doing something." << std::endl;
    }

    // 可选:提供显式销毁接口(测试/热重载场景)
    // 注意:一般不建议手动销毁,让程序退出时自动清理更安全
    static void Destroy() {
        // 无法直接销毁局部静态变量,但可通过标志位"逻辑销毁"
        // 实际项目中通常不需要此方法
    }

private:
    /**
     * @brief 模拟初始化逻辑(可能失败)
     * @return bool true=成功,false=失败
     * 
     * 实际场景:
     * - 加载配置文件
     * - 建立数据库连接
     * - 初始化线程池/网络模块
     */
    bool Init() {
        // 示例:模拟初始化逻辑
        // if (config_not_found) return false;
        return true;  // 默认成功
    }

    /**
     * @brief 私有构造函数:防止外部创建
     * @throw std::runtime_error 初始化失败时抛出
     * 
     * 关键:构造函数抛异常时:
     * - 局部静态变量 instance 的初始化标记为"未完成"
     * - 下次调用 GetInstance() 时会重新进入构造函数
     * - 已分配的资源由构造函数内部负责清理(RAII)
     */
    Singleton() {
        if (!Init()) {
            // 抛出异常前,确保已分配的资源已释放(RAII 原则)
            throw std::runtime_error("Singleton initialization failed");
        }
        std::cout << "Singleton constructed." << std::endl;
    }

    /**
     * @brief 析构函数:自动资源清理
     * 
     * 调用时机:
     * - 程序正常退出时(main 返回或 std::exit)
     * - 按"与构造顺序相反"的顺序销毁静态对象
     */
    ~Singleton() {
        // 释放资源:关闭连接、停止线程、flush 日志等
        std::cout << "Singleton destructed." << std::endl;
    }
    
    // ========== 禁用拷贝语义(单例核心要求)==========
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
};
相关推荐
hwtwhy2 小时前
【情人节特辑】C 语言实现浪漫心形粒子动画(EasyX 图形库)
c语言·开发语言·c++·学习·算法
日月云棠2 小时前
UE5 打包后 EXE 程序单实例的两种实现方法
前端·c++
XiaoHu02072 小时前
MySQL基础(第一弹)
数据库·c++·mysql
Mr YiRan2 小时前
C++浅拷贝与深拷贝的原理
c++
plus4s3 小时前
2月21日(91-93题)
c++·算法
闻缺陷则喜何志丹3 小时前
【数论 等差数列】P9183 [USACO23OPEN] FEB B|普及+
c++·数学·数论·等差数列
拳里剑气3 小时前
C++ 11
开发语言·c++·学习方法
孞㐑¥3 小时前
算法—穷举,爆搜,深搜,回溯,剪枝
开发语言·c++·经验分享·笔记·算法
黄昏晓x3 小时前
C++----异常
android·java·c++