单例模式: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)
相关推荐
CodeYp201915 小时前
第15讲:二叉搜索树
开发语言·c++·二叉搜索树
pipixia2003316 小时前
C++继承面试四连问:内存叠加、私有继承、sizeof、权限重映射
c++·面试·嵌入式·继承·面向对象·对象模型
昭昭日月明16 小时前
AI 时代的《重构》
设计模式·架构
牵猫散步的鱼儿19 小时前
C++ 内存分区
c++
choumin19 小时前
C++ 标准容器为什么不能存引用
c++·容器·引用类型
谙弆悕博士19 小时前
用 C 写了个 Windows 聊天室,踩了几个多线程和控制台的坑
c语言·开发语言·c++·windows·网络编程·聊天室
stolentime20 小时前
CF2277E Tree Guessing题解
c++·算法·贪心算法·交互·ad-hoc
syagain_zsx21 小时前
ODB 与 MySQL:C++ 里的 ORM 实践(类即表、事务铁律与查询封装)
开发语言·数据库·c++·mysql·工具库·1024程序员节
小鱼仙官21 小时前
使用Qt封装C/C++ 动态库 用于第三方调用
c语言·c++·qt
晚风予卿云月21 小时前
【C++高并发内存池】 1.定长内存池的实现
开发语言·c++·内存管理