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

在 C++11 及以后版本中,静态局部变量的初始化是线程安全的,这使得实现线程安全的单例变得异常简单。下面演示两种常见实现方式,并说明它们的优缺点。


1. Meyers 单例(局部静态变量)

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

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

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

关键点

关键点 说明
局部静态变量 C++11 起保证初始化线程安全,多个线程首次访问时只会有一次构造。
延迟初始化 只有第一次调用 getInstance() 时才会构造,符合“按需加载”。
易于维护 代码最短,几乎不需要手动管理互斥量。

注意事项

  • 全局析构:若实例的析构需要在程序结束时执行,确保没有悬挂引用。C++11 标准保证在 main 结束后销毁局部静态变量。
  • 多线程测试:在高并发环境下,仍需验证是否有死锁或资源竞争,虽然标准保证安全,但编译器实现必须符合规范。

2. 双重检查锁(Double-Check Locking, DCL)

class Singleton {
public:
    static Singleton* getInstance() {
        if (instance_ == nullptr) {
            std::lock_guard<std::mutex> lock(mutex_);
            if (instance_ == nullptr) {
                instance_ = new Singleton();
            }
        }
        return instance_;
    }

    // 删除拷贝构造与赋值
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

    ~Singleton() { delete instance_; }

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

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

关键点

关键点 说明
双重检查 先快速检查实例是否已存在,减少锁竞争。
显式互斥 使用 std::mutex 保护实例创建过程。
手动析构 需要手动删除实例,或使用 std::unique_ptr 自动管理。

缺点

  • 实现复杂:代码量大,容易出现错误(如忘记 volatile 或指令重排导致的问题)。
  • 不推荐:自 C++11 起 std::call_once 或局部静态变量更安全、更简洁。

3. 使用 std::call_once

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

    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

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

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

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

关键点

  • std::once_flag:保证闭包只执行一次,即使多线程并发访问。
  • 简洁性:相比 DCL,代码更短,易于维护。

4. 何时选用哪种实现?

场景 推荐实现
简单项目 Meyers 单例(局部静态变量)
需要手动析构 std::call_once + 手动 delete
兼容旧标准(C++03) 双重检查锁 + pthread_oncestd::call_once(Boost)
线程安全 + 延迟初始化 所有实现均满足,选择最简洁的即可

5. 小结

  • C++11 引入的线程安全的局部静态变量让单例实现变得无比简单和可靠。
  • 传统的双重检查锁实现虽然可行,但更易出错,且已被更现代、更安全的 std::call_once 或局部静态变量所取代。
  • 选用何种实现取决于项目对构造/析构时机、代码简洁性以及对旧标准的兼容需求。

如需进一步了解 C++ 中的多线程同步机制,可继续探讨 std::mutex, std::shared_mutex, std::atomic 等工具。

发表评论