c++如何实现单例模式_c++ 饿汉式与懒汉式线程安全实现【方法】

饿汉式单例启动即初始化,天然线程安全;懒汉式首次调用才创建,需用std::call_once或双重检查锁定保障线程安全,但易出错,推荐优先使用饿汉式。

饿汉式单例:启动即初始化,天然线程安全

饿汉式在程序加载时就完成实例构造,后续所有调用都直接返回已创建的对象指针,不存在多线程竞争问题,无需加锁。

关键点在于 static 成员变量的初始化时机由编译器保证——C++11 起,static 局部变量的初始化是线程安全的;而静态成员变量(如类内定义的 static Instance*)在 main() 执行前完成,且仅一次。

常见错误是把指针声明和 new 拆开写,导致非原子操作:

class Singleton {
private:
    static Singleton* instance;
    Singleton() = default;  // 防止外部构造
public:
    static Singleton* getInstance() {
        return instance;  // ❌ instance 可能为 nullptr 或未初始化
    }
};
Singleton* Singleton::instance = new Singleton();  // ✅ 此行才真正构造

更推荐写法(C++11+):

立即学习“C++免费学习笔记(深入)”;

class Singleton {
private:
    Singleton() = default;
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
public:
    static Singleton& getInstance() {
        static Singleton instance;  // ✅ 局部静态变量,线程安全初始化
        return instance;
    }
};
  • 必须禁用拷贝构造与赋值,否则可能意外复制出多个对象
  • 返回引用比返回指针更安全,避免用户误删或置空
  • 析构顺序不可控:局部静态对象在 main 结束后按逆序销毁,若其他静态对象依赖它,可能访问已析构对象

懒汉式单例:首次调用才创建,需手动保障线程安全

懒汉式延迟资源占用,但 getInstance() 中的判空 + 构造逻辑不是原子操作,多线程下极易出现重复 new 或返回未完全构造的对象。

典型错误写法(双重检查锁定漏锁):

static Singleton* getInstance() {
    if (instance == nullptr) {           // 第一次检查
        instance = new Singleton();      // ❌ 构造+赋值非原子,可能被重排,其他线程看到半初始化对象
    }
    return instance;
}

正确实现(C++11 double-checked locking pattern):

class Singleton {
private:
    static std::atomic instance;
    static std::mutex mtx;
    Singleton() = default;
public:
    static Singleton* getInstance() {
        Singleton* tmp = instance.load(std::memory_order_acquire);
        if (tmp == nullptr) {
            std::lock_guard lock(mtx);
            tmp = instance.load(std::memory_order_relaxed);
            if (tmp == nullptr) {
                tmp = new Singleton();
                instance.store(tmp, std::memory_order_release);
            }
        }
        return tmp;
    }
};
std::atomic Singleton::instance{nullptr};
std::mutex Singleton::mtx;
  • 必须用 std::atomic 替代裸指针,否则无法防止指令重排
  • memory_order_acquirememory_order_release 保证构造完成后再对其他线程可见
  • 两次判空缺一不可:第一次避免无谓加锁,第二次防止加锁后已被其他线程创建
  • 不建议手写 DCLP —— 容易出错,优先用局部静态变量(饿汉式)或 std::call_once

更现代的懒汉式替代:std::call_once + once_flag

相比手写 DCLP,std::call_once 更简洁、不易出错,且由标准库保证绝对只执行一次。

class Singleton {
private:
    static Singleton* instance;
    static std::once_flag init_flag;
    Singleton() = default;
public:
    static Singleton* getInstance() {
        std::call_once(init_flag, []() {
            instance = new Singleton();
        });
        return instance;
    }
};
Singleton* Singleton::instance = nullptr;
std::once_flag Singleton::init_flag;
  • std::call_once 内部已做线程同步,无需额外锁或原子操作
  • 适合初始化逻辑较重、且确实需要延迟加载的场景
  • 注意:instance 仍需声明为 static,且不能在 lambda 外提前使用
  • 析构仍需手动管理(比如用 std::unique_ptr 包裹并注册 atexit),否则内存泄漏

饿汉式 vs 懒汉式:选型关键看初始化成本与依赖关系

饿汉式看似“浪费”,实则规避了绝大多数线程安全陷阱;懒汉式看似灵活,却把复杂性推给了开发者。

真实项目中容易被忽略的点:

  • 如果单例构造函数中调用了其他尚未初始化的全局对象(比如另一个单例),饿汉式可能因静态初始化顺序未定义而崩溃
  • 懒汉式若用 std::call_once,其内部实现依赖 OS 级同步原语,在极低概率下(如 fork 后)可能异常,但绝大多数场景可忽略
  • C++20 引入 constinit,但目前对单例帮助有限,仍无法解决跨编译单元初始化顺序问题
  • 真正需要懒汉式的场景极少——多数所谓“耗资源”其实是错觉,真正瓶颈往往在 I/O 或网络,而非内存分配

除非明确知道构造开销极大、且确定不会引发静态初始化依赖,否则默认用饿汉式(局部静态变量版本)最省心。