单例模式是一种创建型设计模式,确保一个类只有一个实例,并提供全局访问点。实现方式主要分为懒汉式(Lazy Initialization)和饿汉式(Eager Initialization),两者的核心区别在于实例化时机和线程安全性。

一、懒汉式(Lazy Initialization)

特点:

  • 延迟实例化:直到第一次调用getInstance()时才创建实例。
  • 资源优化:适用于实例创建代价高或可能不被使用的场景。
  • 线程安全需额外处理:原生实现非线程安全,需通过锁机制或 C++11 特性保证安全。
1. 基础懒汉式(非线程安全)
class LazySingleton {
private:
    static LazySingleton* instance;
    LazySingleton() = default;  // 私有构造函数
    ~LazySingleton() = default;
    LazySingleton(const LazySingleton&) = delete;             // 禁用拷贝构造
    LazySingleton& operator=(const LazySingleton&) = delete;  // 禁用赋值运算符

public:
    static LazySingleton* getInstance() {
        if (instance == nullptr) {  // 第一次检查:若实例未创建
            instance = new LazySingleton();  // 创建实例
        }
        return instance;
    }
};

LazySingleton* LazySingleton::instance = nullptr;  // 静态成员初始化

问题:

  • 线程不安全:多线程环境下,若多个线程同时进入if (instance == nullptr),可能创建多个实例。
2. 线程安全的懒汉式(双重检查锁定,DCL)
#include <mutex>

class LazySingleton {
private:
    static LazySingleton* instance;
    static std::mutex mutex_;
    LazySingleton() = default;
    ~LazySingleton() = default;
    LazySingleton(const LazySingleton&) = delete;
    LazySingleton& operator=(const LazySingleton&) = delete;

public:
    static LazySingleton* getInstance() {
        if (instance == nullptr) {  // 第一次检查(无锁):提高性能
            std::lock_guard<std::mutex> lock(mutex_);  // 加锁
            if (instance == nullptr) {  // 第二次检查(有锁):确保线程安全
                instance = new LazySingleton();
            }
        }
        return instance;
    }
};

LazySingleton* LazySingleton::instance = nullptr;
std::mutex LazySingleton::mutex_;

原理:

  • 第一次检查:无锁快速判断实例是否已创建,减少锁竞争开销。
  • 第二次检查:加锁后再次验证,确保只有一个线程创建实例。
  • 内存屏障:C++11 后编译器会自动优化,避免指令重排序导致的问题(如instance先赋值但对象未完全构造)。
3. C++11 Meyers 单例(推荐实现)
class LazySingleton {
private:
    LazySingleton() = default;
    ~LazySingleton() = default;
    LazySingleton(const LazySingleton&) = delete;
    LazySingleton& operator=(const LazySingleton&) = delete;

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

优势:

  • 自动线程安全:C++11 标准规定静态局部变量的初始化是线程安全的,无需手动加锁。
  • 自动释放:静态变量在程序结束时自动析构,无需担心内存泄漏。
  • 代码简洁:避免了双重检查的复杂性。

二、饿汉式(Eager Initialization)

特点:

  • 立即实例化:在程序启动(静态变量初始化阶段)时就创建实例。
  • 线程安全:由静态变量初始化机制保证,无需额外同步。
  • 资源占用:无论是否使用,实例始终存在,可能浪费资源。
1. 基础饿汉式
class EagerSingleton {
private:
    static EagerSingleton instance;  // 静态实例
    EagerSingleton() = default;
    ~EagerSingleton() = default;
    EagerSingleton(const EagerSingleton&) = delete;
    EagerSingleton& operator=(const EagerSingleton&) = delete;

public:
    static EagerSingleton& getInstance() {
        return instance;
    }
};

EagerSingleton EagerSingleton::instance;  // 程序启动时初始化

关键点:

  • 静态成员变量instance在程序进入main()前就完成初始化,由编译器保证线程安全。
  • 适用于实例创建开销小且必定会被使用的场景。
2. 静态指针版饿汉式
class EagerSingleton {
private:
    static EagerSingleton* instance;
    EagerSingleton() = default;
    ~EagerSingleton() = default;
    EagerSingleton(const EagerSingleton&) = delete;
    EagerSingleton& operator=(const EagerSingleton&) = delete;

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

EagerSingleton* EagerSingleton::instance = new EagerSingleton();  // 程序启动时初始化

区别:

  • 使用指针而非引用,需手动管理内存(但静态指针在程序结束时自动释放)。
  • 延迟了解引用操作(*instance),可能略微提高性能。

三、对比与选择

维度懒汉式(Meyers 单例)饿汉式
实例化时机第一次调用getInstance()时程序启动时(静态初始化阶段)
线程安全性无需额外同步(C++11 保证)天然线程安全(静态变量特性)
资源占用按需创建,避免浪费始终占用内存,即使未使用
创建失败处理可在运行时捕获异常(如内存不足)初始化失败可能导致程序无法启动
复杂度中等(需理解静态局部变量特性)低(简单直接)
适用场景实例创建开销大、可能不被使用的场景实例创建开销小、程序启动时必须存在

四、进阶问题

1. 单例实例的销毁
  • 饿汉式:静态实例由系统自动释放,无需处理。
  • 懒汉式(指针版):需确保在程序结束前释放资源,可通过atexit()注册销毁函数:
    static void destroyInstance() {
        delete instance;
        instance = nullptr;
    }
    // 在getInstance()中注册
    std::atexit(destroyInstance);
    
2. 多进程 / 多 DLL 场景
  • 单例模式仅保证单进程内唯一,多进程环境需通过共享内存或文件锁实现跨进程单例。
  • 在 DLL 中使用单例时,需注意不同模块可能加载不同的 DLL 实例,导致多个单例存在。
3. 替代方案
  • 依赖注入(DI):通过参数传递实例,避免全局状态,提高可测试性。
  • 静态工具类:若无需维护状态,可使用静态方法替代单例(如MathUtils)。

五、面试高频问题

  1. 为什么懒汉式需要双重检查锁定?

    • 答:第一次检查提高性能(无锁快速判断),第二次检查确保线程安全(加锁后验证)。
  2. 饿汉式是否绝对线程安全?

    • 答:在单进程环境下是线程安全的,但若涉及动态链接库(DLL)加载,可能在多模块中创建多个实例。
  3. Meyers 单例的线程安全是如何实现的?

    • 答:C++11 标准规定,静态局部变量的初始化在多线程环境下是线程安全的,编译器会自动生成同步代码。
  4. 单例模式如何防止反射攻击?

    • 答:在 Java 中可通过枚举实现单例(天然防止反射),C++ 中需在构造函数中检查实例是否已存在,若存在则抛出异常。
  5. 单例模式的优缺点?

    • 优点:控制资源、全局访问、避免重复创建。
    • 缺点:违反单一职责、单测困难、可能导致内存泄漏。

更多推荐