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

在现代 C++(C++11 及以后)中实现线程安全的单例模式已经不再是一个复杂的问题。标准库为我们提供了几种非常优雅、性能友好且易于维护的实现方式。下面将分别介绍三种常见实现方式:Meyer’s 单例、双重检查锁定(Double-Checked Locking)以及使用 std::call_once 的单例。我们还会讨论每种实现的适用场景、优缺点,并给出完整可编译的示例代码。


1. Meyer’s 单例(静态局部变量)

思路

利用 C++11 对局部静态变量初始化的线程安全保证,Meyer’s 单例实现非常简洁。局部静态对象在第一次进入函数时才被构造,并且保证在多线程环境下只会被构造一次。

代码

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

    // 禁止拷贝构造和赋值
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

    void do_something() {
        // 业务逻辑
    }

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

优点

  • 代码最简洁,几乎没有额外的锁或同步开销。
  • 只要使用 static 变量,C++11 标准就保证线程安全。
  • 延迟实例化:只有第一次调用 instance() 时才会构造对象,节省启动时资源。

缺点

  • 对构造异常不够友好。如果构造时抛出异常,后续的调用会再次尝试构造,可能导致不一致的状态。
  • 对析构时机不易控制。若需要在程序结束前手动销毁,难以做到。

2. 双重检查锁定(Double-Checked Locking)

思路

使用 std::mutex 与双重检查锁定模式,在首次调用时进行同步构造,后续调用跳过锁以减少锁的开销。

代码

class Singleton {
public:
    static Singleton* instance() {
        // 第一检查,未初始化则进入同步块
        if (!instance_) {
            std::lock_guard<std::mutex> lock(mutex_);
            // 第二检查,避免多线程并发初始化
            if (!instance_) {
                instance_ = new Singleton();
            }
        }
        return instance_;
    }

    // 释放资源
    static void destroy() {
        std::lock_guard<std::mutex> lock(mutex_);
        delete instance_;
        instance_ = nullptr;
    }

    // 业务方法
    void do_something() { /* ... */ }

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

    static Singleton* instance_;
    static std::mutex mutex_;
};

// 头文件中的定义
Singleton* Singleton::instance_ = nullptr;
std::mutex Singleton::mutex_;

优点

  • 可以显式控制单例的销毁时机,避免程序退出时静态对象析构顺序问题。
  • 适用于对构造失败需要做重试或需要在程序退出前释放资源的情况。

缺点

  • 需要显式销毁,使用不当会导致内存泄漏。
  • 代码相对复杂,易出错。
  • 仍然需要在每次调用时检查指针,稍有性能损耗。

3. std::call_oncestd::once_flag

思路

std::call_oncestd::once_flag 提供了更简洁的单例构造方式,既保证线程安全,又避免了手动使用 mutex 的繁琐。

代码

class Singleton {
public:
    static Singleton& instance() {
        std::call_once(initFlag_, []{
            instance_ = new Singleton();
        });
        return *instance_;
    }

    static void destroy() {
        std::call_once(destroyFlag_, []{
            delete instance_;
            instance_ = nullptr;
        });
    }

    void do_something() { /* ... */ }

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

    static Singleton* instance_;
    static std::once_flag initFlag_;
    static std::once_flag destroyFlag_;
};

// 头文件中的定义
Singleton* Singleton::instance_ = nullptr;
std::once_flag Singleton::initFlag_;
std::once_flag Singleton::destroyFlag_;

优点

  • 代码更简洁,避免了手动 mutex 的错误。
  • 仍然支持显式销毁,适用于资源清理需求。

缺点

  • std::call_once 一样,若构造抛异常,会导致后续 instance() 调用再次尝试初始化。
  • 需要显式销毁,防止内存泄漏。

4. 对比与选择

实现方式 线程安全保证 代码简洁 析构控制 适用场景
Meyer’s 单例 C++11 标准保证 轻量级、不需显式销毁
双重检查锁定 手动 mutex 需要手动销毁或特殊异常处理
std::call_once 标准保证 需要手动销毁且想避免手动 mutex
  • 如果你只需要一个全局唯一对象,且不关心手动销毁,推荐使用 Meyer’s 单例
  • 如果你需要在程序退出前释放资源或避免析构顺序问题,可以选择 std::call_once双重检查锁定,并显式提供销毁函数。
  • 如果你对构造失败需要特殊重试或恢复逻辑,使用 std::call_once 也更容易加入错误处理。

5. 小结

C++11 之后实现线程安全单例变得异常简单。利用语言本身对局部静态对象初始化的线程安全保证,或者使用 std::call_once,都能让代码保持简洁且可靠。你只需根据项目的资源管理需求和异常处理逻辑选择最合适的实现即可。祝编码愉快!

发表评论