单例模式: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)
相关推荐
爱和冰阔落35 分钟前
【Linux】Ctrl+C 到底做了什么?从键盘、kill 到 alarm,一次讲清信号的产生
linux·运维·c++·安卓
水饺编程1 小时前
第5章,[Win32 章节] :贝塞尔样条曲线(一)
c语言·c++·windows·visual studio
程序员打怪兽2 小时前
C++:类与对象,封装,继承,多态,构造/析构,重载/重写
c++
Escalating_xu2 小时前
【C++ STL简介】从六大组件到容器、迭代器与算法协作
java·c++·算法
Brilliantwxx2 小时前
【Linux】 进程(3)深度解析:从查看进程到进程状态
linux·服务器·网络·c++
啊啊啊啊啊!!!!2 小时前
【c++】二叉搜索树
开发语言·c++
ShineWinsu3 小时前
对于 C++:C++20中Concept(概念) 与 Coroutine(协程)的解析
linux·开发语言·网络·c++·c++20·epoll
liulilittle3 小时前
llmx 学习手册 06 —— CPU 指令集优化(AVX-512 三层演进)
c++·学习·算法·ai·llm
skr爱码士3 小时前
05_Qt 核心模块概览——Qt Core、Gui、Widgets、Quick 的职责划分
c++·qt·系统架构·客户端
郝学胜-神的一滴4 小时前
Horse3D 游戏引擎研发笔记(七):Clydesdale——从流式日志到多输出订阅
c++·qt·unity·游戏引擎·图形渲染·unreal engine·opengl