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

在多线程环境下实现线程安全的单例模式是C++编程中常见的需求。下面介绍几种主流实现方式,并讨论它们的优缺点。

1. 经典双重检查锁(DCLP)

class Singleton {
public:
    static Singleton& getInstance() {
        if (instance_ == nullptr) {              // 第一次检查
            std::lock_guard<std::mutex> lock(mutex_);
            if (instance_ == nullptr) {          // 第二次检查
                instance_ = new Singleton();
            }
        }
        return *instance_;
    }
private:
    Singleton() = default;
    ~Singleton() = default;
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

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

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

优点

  • 在单线程或已经初始化的多线程场景下性能几乎与普通函数调用相同。

缺点

  • 在C++11之前,存在指令重排导致线程看到部分初始化的对象。
  • 需要显式销毁(如果不销毁则可能造成资源泄露)。
  • 代码稍显繁琐。

2. C++11的局部静态变量(Meyer’s Singleton)

class Singleton {
public:
    static Singleton& getInstance() {
        static Singleton instance;   // 第一次调用时初始化
        return instance;
    }
private:
    Singleton() = default;
    ~Singleton() = default;
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
};

优点

  • 简单、直观。
  • 标准保证了初始化的线程安全(C++11以后)。
  • 资源在程序结束时自动销毁,避免泄露。

缺点

  • 对象在第一次访问时创建,可能导致启动延迟。
  • 不能控制析构顺序,若单例在其他静态对象之前销毁,后者访问时可能出现问题。

3. std::call_oncestd::once_flag

class Singleton {
public:
    static Singleton& getInstance() {
        std::call_once(flag_, [](){ instance_ = new Singleton(); });
        return *instance_;
    }
    static void destroy() {
        std::lock_guard<std::mutex> lock(mutex_);
        delete instance_;
        instance_ = nullptr;
    }
private:
    Singleton() = default;
    ~Singleton() = default;
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

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

Singleton* Singleton::instance_ = nullptr;
std::once_flag Singleton::flag_;
std::mutex Singleton::mutex_;

优点

  • 明确控制单例的创建时机。
  • 结合 destroy() 可以安全销毁。

缺点

  • 仍需要手动管理销毁。
  • 代码相对更复杂。

4. 线程安全的懒加载使用 std::shared_ptr

class Singleton {
public:
    static std::shared_ptr <Singleton> getInstance() {
        std::call_once(flag_, [](){
            instance_ = std::shared_ptr <Singleton>(new Singleton());
        });
        return instance_;
    }
private:
    Singleton() = default;
    ~Singleton() = default;
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

    static std::shared_ptr <Singleton> instance_;
    static std::once_flag flag_;
};

std::shared_ptr <Singleton> Singleton::instance_;
std::once_flag Singleton::flag_;

优点

  • 自动管理生命周期,避免泄漏。
  • 对外返回共享指针,使用更灵活。

缺点

  • 每次返回都要复制 shared_ptr 的引用计数,成本略高。

5. 选型建议

需求 推荐实现 说明
简单、快速 Meyer’s Singleton(局部静态) C++11后线程安全,最简洁
需要显式销毁 std::call_once + 手动 destroy() 可控制生命周期
需要懒加载且自动管理 std::shared_ptr + std::call_once 自动销毁,适用于复杂生命周期
兼容旧标准 DCLP(注意指令重排) 需要手动处理重排,慎用

6. 常见陷阱

  1. 多次销毁
    destroy() 调用多次或在多线程环境中并发调用,需加锁或使用原子操作防止双删。

  2. 析构顺序
    静态对象在 Meyer's 单例前析构,后续访问会出现悬空指针。可在 atexit 注册销毁或使用 std::shared_ptr

  3. 构造抛异常
    std::call_once 在异常后会重试,但 Meyer's 直接抛异常,可能导致程序中断。根据业务决定是否捕获异常。

  4. 调试与日志
    单例隐藏了对象的生命周期,调试时需注意初始化与销毁时机。可在构造/析构中输出日志。


小结

C++11 引入的局部静态变量和 std::call_once 使得实现线程安全的单例变得极为简洁可靠。选择哪种实现方式,取决于是否需要显式销毁、是否兼容旧标准以及对性能的要求。正确理解和运用这些技术,可以让你在多线程程序中安全、高效地使用单例模式。

发表评论