本文适合:具备一定 C++ 基础,正在学习设计模式或准备面试的后端开发者。

阅读收获

  1. 深入掌握单例模式的五种实现方式
  2. 理解饿汉式内存泄漏的真正原因
  3. 认识指针单例的架构价值
  4. 掌握 Meyers' 单例作为 C++11 最佳实践的原理

一、单例模式核心概念

单例模式(Singleton Pattern) 是最基础、最常用的创建型设计模式。其核心约束可概括为:

确保类在程序生命周期内仅存在唯一实例,并提供全局访问点。

实现这一约束需同时满足四个关键条件:

  1. 私有化构造函数 - 禁止外部通过 new 或栈上声明创建实例
  2. 禁用拷贝构造与赋值运算符 - 防止通过复制产生新实例
  3. 提供静态全局访问接口 - 统一实例获取入口
  4. 保证线程安全 - 确保多线程环境下实例唯一性

下面我们将从经典的饿汉式入手,逐步剖析不同实现方式的细节。


二、饿汉式单例(Eager Initialization)

2.1 核心特性

饿汉式的核心理念是提前初始化

  • 类加载时即创建实例(程序启动即存在)
  • 天生线程安全(C++ 静态变量初始化由运行时保证线程安全)
  • 实现简单,无锁,性能优异
  • 缺点:无论是否使用都会占用内存,不适合大型对象

2.2 静态对象版(推荐,无内存泄漏)

#include <iostream>
using namespace std;

class SingletonEager {
private:
    // 1. 私有化构造函数
    SingletonEager() {
        cout << "饿汉单例:构造函数执行" << endl;
    }

    // 2. 禁用拷贝和赋值
    SingletonEager(const SingletonEager&) = delete;
    SingletonEager& operator=(const SingletonEager&) = delete;

    // 3. 静态实例:程序启动即创建
    static SingletonEager instance;

public:
    // 4. 全局访问接口
    static SingletonEager& getInstance() {
        return instance;
    }

    void doSomething() {
        cout << "饿汉单例工作中..." << endl;
    }
};

// 类外初始化:程序启动时创建实例
SingletonEager SingletonEager::instance;

int main() {
    SingletonEager& s1 = SingletonEager::getInstance();
    SingletonEager& s2 = SingletonEager::getInstance();

    cout << "s1地址: " << &s1 << endl;
    cout << "s2地址: " << &s2 << endl;  // 地址相同

    s1.doSomething();
    return 0;
}

该版本的 instance 为静态对象,存储在程序的数据段(BSS/Data Segment)。其生命周期为:程序启动创建 → 程序结束自动销毁。全程自动管理,不存在内存泄漏风险

2.3 静态指针版(存在内存泄漏风险)

class SingletonEagerPtr {
private:
    SingletonEagerPtr() = default;
    SingletonEagerPtr(const SingletonEagerPtr&) = delete;
    SingletonEagerPtr& operator=(const SingletonEagerPtr&) = delete;

    static SingletonEagerPtr* instance;

public:
    static SingletonEagerPtr* getInstance() {
        return instance;
    }
};

// 程序启动即 new
SingletonEagerPtr* SingletonEagerPtr::instance = new SingletonEagerPtr();

此版本将对象改为指针,底层行为发生本质变化:

  • instance 为指向堆内存的指针
  • 程序结束时指针变量自动销毁,但堆对象不会自动释放
  • 必须手动 delete 否则导致内存泄漏

三、饿汉式内存泄漏的真相

常见误区是笼统认为"饿汉式会内存泄漏",实际需要区分两种实现:

实现方式 内存位置 销毁方式 内存泄漏风险
静态对象 数据段 程序结束自动销毁 无泄漏
静态指针 堆(new分配) 必须手动 delete 有泄漏风险

核心结论:内存泄漏并非饿汉式本身的问题,而是由"指针+new"的用法导致。

静态对象版饿汉式是安全的,其生命周期与全局变量一致;指针版需要手动管理内存,否则会造成泄漏。


四、饿汉式的架构局限:预占用内存

饿汉式在程序启动时即创建对象,无论后续是否使用。这在处理大型对象时可能造成资源浪费:

示例场景

  • 100MB 配置文件解析器
  • 500MB 数据库连接池

对比分析

  • 饿汉式:程序启动即占用600MB内存,即使从未使用
  • 懒汉式:首次使用时才分配内存,不使用则不占用

底层机制:

// 在main()执行前即完成初始化
SingletonEager SingletonEager::instance;

静态对象初始化由C++运行时在程序入口前完成,这意味着即使从未调用 getInstance(),对象也已存在。

适用场景建议

类型 初始化时机 内存占用特点 适用场景
饿汉式 程序启动时 始终占用 轻量级工具类
懒汉式 首次调用时 按需占用 大型资源类

五、懒汉式单例(Lazy Initialization)

5.1 核心特性

懒汉式的核心理念是延迟初始化

  • 按需创建:首次调用时实例化
  • 节省内存:不使用则不占用资源
  • 基础版非线程安全:多线程可能创建多个实例
  • 需加锁保证线程安全

5.2 基础懒汉式(非线程安全,教学示例)

class SingletonLazy {
private:
    SingletonLazy() {
        cout << "懒汉单例:构造函数执行" << endl;
    }

    SingletonLazy(const SingletonLazy&) = delete;
    SingletonLazy& operator=(const SingletonLazy&) = delete;

    static SingletonLazy* instance;

public:
    static SingletonLazy* getInstance() {
        if (instance == nullptr) {  // 无锁检查:多线程风险点
            instance = new SingletonLazy();
        }
        return instance;
    }

    void doSomething() {
        cout << "懒汉单例工作中..." << endl;
    }
};

SingletonLazy* SingletonLazy::instance = nullptr;

致命缺陷

多线程同时执行 if (instance == nullptr) 时可能都判断为 true,导致各自创建实例,破坏单例约束。该实现在生产环境中严禁使用


六、双重检查锁单例(DCLP)

6.1 核心特性

双重检查锁定模式(DCLP)是懒汉式的线程安全优化版本,具备以下特点:

  • 双重判空 + 互斥锁:在保证线程安全的同时避免每次调用都加锁
  • 架构设计中首选的懒加载方案之一
  • C++11 起需配合 volatile 或内存屏障防止指令重排

6.2 实现代码

#include <iostream>  
#include <mutex>  
using namespace std;  

class SingletonDCLP {  
private:  
    SingletonDCLP() {  
        cout << "双重锁单例:构造函数执行" << endl;  
    }  

    SingletonDCLP(const SingletonDCLP&) = delete;  
    SingletonDCLP& operator=(const SingletonDCLP&) = delete;  

    static volatile SingletonDCLP* instance;  
    static mutex mtx;  

public:  
    static SingletonDCLP* getInstance() {  
        // 第一次检查:无锁快速路径  
        if (instance == nullptr) {  
            lock_guard<mutex> lock(mtx);  

            // 第二次检查:防止竞态条件  
            if (instance == nullptr) {  
                instance = new SingletonDCLP();  
            }  
        }  
        return instance;  
    }  

    void doSomething() {  
        cout << "双重锁单例工作中..." << endl;  
    }  
};  

volatile SingletonDCLP* SingletonDCLP::instance = nullptr;  
mutex SingletonDCLP::mtx;  

6.3 双重检查的必要性

  1. 首次判空:避免绝大多数情况下的加锁操作,保证高性能
  2. 互斥锁:确保只有一个线程进入实例化临界区
  3. 二次判空:防止获得锁的线程重复创建实例

七、C++11 最优解:Meyers' 单例

该实现是 C++ 单例的终极方案,满足:

  • 懒加载
  • 线程安全
  • 无内存泄漏
  • 高性能
class SingletonMeyers {  
private:  
    SingletonMeyers() {  
        cout << "Meyers单例:构造函数执行" << endl;  
    }  

    SingletonMeyers(const SingletonMeyers&) = delete;  
    SingletonMeyers& operator=(const SingletonMeyers&) = delete;  

public:  
    static SingletonMeyers& getInstance() {  
        // C++11 保证静态局部变量初始化线程安全  
        static SingletonMeyers instance;  
        return instance;  
    }  

    void doSomething() {  
        cout << "Meyers单例工作中..." << endl;  
    }  
};  

核心优势

  • 首次访问时初始化,实现懒加载
  • 编译器自动生成线程安全代码
  • 对象存储在静态区,自动管理生命周期
  • 代码简洁高效

八、指针单例的适用场景

8.1 动态生命周期管理

静态对象无法实现:

  • 按需销毁重建(如配置热更新)
  • 资源受限系统的内存释放

8.2 解决静态初始化顺序问题

跨编译单元的静态对象初始化顺序未定义,可能导致启动崩溃。

8.3 支持多态与接口隔离

class ISingleton {  
public:  
    virtual void process() = 0;  
    virtual ~ISingleton() = default;  
};  

class SingletonImpl : public ISingleton {  
    void process() override { /*...*/ }  
};  

ISingleton* SingletonFactory::instance = new SingletonImpl();  

8.4 跨模块兼容性

指针可安全跨越 DLL/SO 模块边界。


九、实现方式对比

实现方式 线程安全 性能 懒加载 内存安全 推荐度
饿汉式(静态对象) ✔️ 极高 ✖️ ✔️ ⭐⭐⭐⭐
饿汉式(指针) ✔️ 极高 ✖️ ✖️ ⭐⭐
基础懒汉式 ✖️ ✔️ ✖️
双重检查锁 ✔️ ✔️ ✖️ ⭐⭐⭐⭐
Meyers' 单例 (C++11+) ✔️ 极高 ✔️ ✔️ ⭐⭐⭐⭐⭐

十、架构实践要点

  1. 禁止继承:子类化会破坏单例约束
  2. 并发场景选择:优先使用 Meyers' 或 DCLP
  3. 显式禁用拷贝:必须声明 = delete
  4. 资源权衡:大对象用懒加载,小对象用饿汉式
  5. 指针管理:配套实现销毁接口或使用智能指针
  6. 初始化顺序:警惕多文件间的静态对象依赖

结论

  • 轻量级场景:优先选用饿汉式静态对象
  • 需要灵活性:采用指针版本控制生命周期
  • 现代 C++:默认使用 Meyers' 单例
  • 内存安全:通过智能指针或静态对象保障

如果本文对您有帮助,欢迎点赞/收藏/关注。技术问题欢迎评论区交流。

更多推荐