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

在多线程环境下,单例模式(Singleton)需要保证仅有一个实例且对所有线程可见。常见的实现方案包括“双重检查锁定(Double-Check Locking)”、使用C++11的局部静态变量,以及使用std::call_once。下面分别演示这三种方法,并讨论其优缺点。

1. 双重检查锁定(Double-Check Locking)

class Singleton {
public:
    static Singleton* Instance() {
        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之前,编译器对内存模型的优化可能导致“实例未完全初始化就被访问”的风险。C++11之后通过原子操作和内存屏障可以安全使用。

2. 局部静态变量(Meyer’s Singleton)

class Singleton {
public:
    static Singleton& Instance() {
        static Singleton instance;  // C++11保证线程安全
        return instance;
    }
private:
    Singleton() = default;
    ~Singleton() = default;
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
};

说明

  • 优点:代码最简洁,编译器自动保证线程安全。
  • 缺点:无法延迟销毁(直到程序结束),不适合需要自定义销毁顺序的场景。

3. std::call_oncestd::once_flag

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

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

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

说明

  • 优点:显式控制初始化时机,兼顾延迟创建与线程安全。
  • 缺点:需要手动管理内存,若不在适当位置调用delete,可能导致泄漏。

4. 比较与选择

方法 延迟加载 线程安全保证 锁消耗 代码简洁性 适用场景
双重检查锁定 ✅(C++11后) 较复杂 需要在老旧编译器下兼容
局部静态 ✅(C++11) 0 快速实现,销毁时机无关
call_once 中等 需要自定义销毁,或与其他初始化逻辑配合

5. 实际应用示例

int main() {
    auto& s1 = Singleton::Instance();
    auto& s2 = Singleton::Instance();

    // 两个引用指向同一个对象
    assert(&s1 == &s2);
}

6. 小结

在C++11及以后,推荐使用局部静态变量实现单例,因为它既简洁又得到标准库的充分支持。若项目有特殊销毁顺序需求或想避免全局对象析构顺序问题,可以使用std::call_once。在需要兼容旧编译器时,可采用双重检查锁定,但需谨慎处理内存模型细节。


发表评论