单例模式:C++实现与多线程安全

1. 核心定义与作用(精准版)

定义

单例模式 是一种创建型设计模式,确保一个类有且仅有一个实例 ,并向整个系统提供唯一的全局访问点

核心作用

  1. 控制实例数量 :严格保证类在程序生命周期内只有一个对象
  2. 全局访问:无需传递对象,在代码任意位置获取同一个实例
  3. 解决实际问题
    • 避免多实例竞争资源(日志文件、数据库连接、配置文件、打印机)
    • 减少频繁创建 / 销毁对象的性能开销
    • 统一数据状态,保证全局数据一致性

2. 实现方式:饿汉式 vs 懒汉式(对比 + 代码)

一、饿汉式(静态初始化)

核心 :类加载时就创建实例,以空间换时间

cpp

复制代码
// 饿汉式单例(线程安全,无需加锁)
class Singleton {
private:
    // 1. 构造函数私有化
    Singleton() = default;
    // 2. 禁用拷贝、赋值
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

    // 3. 静态实例(类加载时初始化)
    static Singleton instance;

public:
    // 4. 全局访问点
    static Singleton& getInstance() {
        return instance;
    }
};

// 类外初始化
Singleton Singleton::instance;

优点

  • 天然线程安全(由静态初始化保证)
  • 无锁竞争,访问速度快
  • 实现简单,无坑

缺点

  • 程序启动就初始化,即使不用也占内存
  • 大对象会延长启动时间
  • 无法做到延迟加载

适用场景:实例小、一定会使用、追求高性能


二、懒汉式(延迟初始化)

核心 :第一次使用时才创建实例,以时间换空间

① 基础版(线程不安全)

cpp

复制代码
static Singleton* getInstance() {
    if (instance == nullptr) {  // 多线程下会多次进入
        instance = new Singleton();
    }
    return instance;
}

问题:高并发下会创建多个实例,破坏单例。


3. 多线程安全与优化(面试核心)

① 加锁版(线程安全,但效率低)

cpp

复制代码
static Singleton* getInstance() {
    std::lock_guard<std::mutex> lock(mtx); // 每次都加锁
    if (instance == nullptr) {
        instance = new Singleton();
    }
    return instance;
}

缺点 :99% 的情况实例已存在,仍要加锁,并发性能极差


② 双重检查锁 DCL(Double-Checked Locking)

面试必考:为什么要两次判断?

  • 第一次判断:避免每次都加锁
  • 第二次判断:防止多线程同时通过第一次判断

cpp

复制代码
static Singleton* getInstance() {
    if (instance == nullptr) {       // 第一次检查(无锁)
        std::lock_guard<std::mutex> lock(mtx);
        if (instance == nullptr) {   // 第二次检查(有锁)
            instance = new Singleton();
        }
    }
    return instance;
}

致命问题:指令重排 new 分为三步:

  1. 分配内存
  2. 调用构造函数
  3. 指针赋值给 instance

编译器可能优化为:1 → 3 → 2导致其他线程拿到未构造完成的半成品对象,程序崩溃。

C++11 解决方案 使用 std::atomic + 内存屏障禁止重排。


③ Meyers 单例(C++ 最优写法,强烈推荐)

C++11 特性 :静态局部变量初始化是线程安全的!

cpp

复制代码
class Singleton {
private:
    Singleton() = default;
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

public:
    static Singleton& getInstance() {
        static Singleton instance; // 线程安全,延迟加载
        return instance;
    }
};

优点

  • 延迟加载
  • 天然线程安全
  • 无锁、无指针、无内存泄漏
  • 代码极简、无坑

这是 C++ 工程与面试首选写法!


4. C++ 单例必写规范(面试必问)

① 必须私有化构造函数

防止外部 new 创建对象。

② 必须禁用拷贝构造 + 赋值运算符

cpp

复制代码
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;

防止通过拷贝产生新实例。

③ 析构函数一般私有化

单例由类管理生命周期,外部不能 delete。

④ 实例类型选择

  • 饿汉:静态对象(栈 / 全局区)
  • 懒汉:静态局部对象(推荐)
  • 不推荐裸指针(内存泄漏风险)

5. 单例生命周期与内存管理

  1. 静态实例 :程序结束时自动释放,无需手动管理
  2. 指针实例 :需要手动写 destroy() 释放,否则内存泄漏
  3. 依赖顺序:多个单例互相依赖时,可能出现析构顺序问题
  4. 最佳方案:使用 Meyers 单例,完全交给系统管理

6. 单例模式优缺点总结

优点

  • 严格控制唯一实例
  • 全局访问方便
  • 避免重复创建销毁开销
  • 避免资源竞争冲突

缺点

  • 扩展性差(很难改成多例)
  • 隐藏依赖关系
  • 多线程下实现复杂(基础版不安全)
  • 不利于单元测试(难以 mock)
相关推荐
NiceCloud喜云22 分钟前
Opus 4.8 的 Effort Control 怎么选:Low 到 Max 五档策略
android·java·大数据·前端·c++·python·spring
cjhbachelor1 小时前
c++继承
c++
肩上风骋1 小时前
C++14特性
开发语言·c++·c++14特性
QiLinkOS4 小时前
【从实验室到商业战场:发明专利如何重塑科技与企业的共生生态】
大数据·c语言·数据结构·c++·人工智能·单片机·算法
Irissgwe5 小时前
c++11(lambda表达式与包装器、线程库)
c++·c++11·lambda表达式·线程库·包装器·互斥量库·条件变量库
Peter·Pan爱编程5 小时前
14. Lambda 表达式:随手可写的函数对象
c++·算法·ai编程
不想写代码的星星6 小时前
从分支预测角度看 C++:为什么你的热循环慢得离谱?
c++
郝学胜-神的一滴6 小时前
Qt 高级开发 018:复刻经典登录界面布局与窗口美化全解析
开发语言·c++·qt·程序人生·用户界面
郝亚军6 小时前
IEEE 754 单精度浮点的SEM表示
开发语言·c++·算法
Yyyyyy~8 小时前
【C++】数组篇
开发语言·c++