如何在C++中实现线程安全的单例模式?

在现代 C++(C++11 及以后)中实现线程安全的单例模式比以往任何时候都简单、可靠。下面从设计思路、实现细节、性能考虑以及常见误区四个方面进行阐述,并给出完整可编译的示例代码。


一、设计思路

  1. 懒初始化
    单例对象只在第一次使用时创建,避免不必要的资源占用。

  2. 线程安全
    需要保证在多线程环境下只创建一个实例,并且在实例创建后对其的访问也是安全的。

  3. 全局访问
    通过 Singleton::instance() 或类似的静态成员函数提供全局入口。

  4. 防止拷贝/移动
    禁用拷贝构造、移动构造和赋值操作,保证只有一个对象存在。


二、实现细节

2.1 传统 Meyers 单例(C++11 以后)

class Singleton {
public:
    static Singleton& instance() {
        static Singleton instance;   // C++11 保证线程安全
        return instance;
    }

    // 业务接口
    void doSomething() { /* ... */ }

private:
    Singleton()  = default;
    ~Singleton() = default;

    // 禁止拷贝、移动
    Singleton(const Singleton&)            = delete;
    Singleton& operator=(const Singleton&) = delete;
    Singleton(Singleton&&)                 = delete;
    Singleton& operator=(Singleton&&)      = delete;
};

优点

  • 代码简洁,几行即可完成
  • C++11 标准保证了 static 局部变量的初始化线程安全

缺点

  • 若单例需要在程序退出前执行自定义清理,可能会出现析构顺序问题(尤其在多翻译单元情况下)

2.2 经典双重检查锁(不推荐)

class Singleton {
public:
    static Singleton* instance() {
        if (!ptr_) {                     // 第一重检查
            std::lock_guard<std::mutex> lock(mutex_);
            if (!ptr_) {                 // 第二重检查
                ptr_ = new Singleton();
            }
        }
        return ptr_;
    }

private:
    static Singleton* ptr_;
    static std::mutex mutex_;
    Singleton() = default;
};

Singleton* Singleton::ptr_ = nullptr;
std::mutex Singleton::mutex_;

不推荐

  • 复杂度高,易出错
  • 需要手动管理内存和析构

2.3 智能指针 + std::call_once(兼顾延迟加载与析构)

class Singleton {
public:
    static Singleton& instance() {
        std::call_once(flag_, []() {
            ptr_.reset(new Singleton());
        });
        return *ptr_;
    }

private:
    Singleton() = default;
    ~Singleton() = default;
    static std::unique_ptr <Singleton> ptr_;
    static std::once_flag flag_;
};

std::unique_ptr <Singleton> Singleton::ptr_;
std::once_flag Singleton::flag_;
  • std::once_flagstd::call_once 结合使用,确保初始化只执行一次
  • unique_ptr 自动释放资源,避免内存泄漏
  • 适用于需要在销毁时做清理或不想使用局部静态对象的场景

三、性能与可测性

  • 局部静态 的实现几乎不占用额外内存,且初始化成本仅一次。
  • std::call_once 也只会在第一次调用时花费少量锁开销,后续调用几乎无锁。
  • 如果单例是高成本对象,建议使用 懒加载 并在必要时显式销毁,或者使用 std::shared_ptr 并将其生命周期与业务对象绑定。

四、常见误区

误区 正确做法
认为 new 后的单例不需要 delete 使用 std::unique_ptr 或局部静态即可自动销毁
直接使用宏 #define SINGLETON 宏无法提供类型安全,易导致命名冲突
忽略拷贝/移动构造 必须显式 delete 相关函数,防止生成多实例
在多文件项目中把单例定义在头文件 需要 inlineconstexpr 静态成员,或者使用单独的源文件实现

五、完整示例(Meyers 单例)

#include <iostream>
#include <string>

class Logger {
public:
    static Logger& get() {
        static Logger instance;   // C++11 线程安全
        return instance;
    }

    void log(const std::string& msg) {
        // 简单示例:输出到标准输出
        std::cout << "[LOG] " << msg << '\n';
    }

private:
    Logger()  = default;
    ~Logger() = default;

    Logger(const Logger&)            = delete;
    Logger& operator=(const Logger&) = delete;
    Logger(Logger&&)                 = delete;
    Logger& operator=(Logger&&)      = delete;
};

int main() {
    Logger::get().log("程序开始");
    Logger::get().log("这是第二条日志");
    return 0;
}
  • Logger 是单例日志类,任何线程均可安全调用 log()
  • 由于局部静态的实现,日志实例在第一次 get() 时创建,程序退出时自动销毁。
  • 禁用拷贝/移动确保了单例唯一性。

六、总结

  • 在 C++11 之后,Meyers 单例(局部静态)已足以满足大多数需求,代码简洁且线程安全。
  • 若需要自定义销毁顺序或更细粒度的控制,可结合 std::call_once + std::unique_ptr
  • 始终禁用拷贝/移动,避免多实例。
  • 关注单例的生命周期与资源管理,防止内存泄漏或析构顺序问题。

掌握上述模式后,你就能在 C++ 项目中安全、灵活地使用单例。祝编码愉快!

发表评论