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

17次阅读

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

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

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

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

关键点在于 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 或网络,而非内存分配

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

text=ZqhQzanResources