单例模式: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)
相关推荐
jimy114 分钟前
虚函数vtable,链接时看到构造函数,linker把符号地址写入vtable(三)
开发语言·c++
潼心1412o1 小时前
C++初阶(长期更新)第9讲:string(上)
开发语言·c++
朝朝辞暮i1 小时前
C++ 第 39 章: 阶段性总复习——类、对象、构造、继承、this、指针与智能指针
开发语言·c++·算法·ros2
wuminyu2 小时前
Virtual Thread重投递至ForkJoinPool任务队列过程解析
java·linux·c语言·jvm·c++
All for pursuit.2 小时前
【回溯-6】79.单词搜索
数据结构·c++·算法·leetcode
Benny_Tang3 小时前
AT_arc180_d [ARC180D] Division into 3 题解
数据结构·c++·算法
殷色玫瑰3 小时前
C++ STL:stack、queue、priority_queue 与容器适配器详解
java·开发语言·数据结构·c++·算法·visualstudio
努力努力再努力wz3 小时前
【CUDA入门系列】CUDA编程实战:设备选择、显存管理、Kernel、Stream 与 Event 全流程梳理
开发语言·c++
沫璃染墨4 小时前
《从零入门Linux系统篇(五十八):线程篇·十一——线程安全与死锁详解:从可重入到多锁管理》
linux·服务器·开发语言·c++·驱动开发·安全·架构
郝学胜_神的一滴4 小时前
C++ Templates 05:那些容易踩坑的技巧性基础知识
c++·visual studio