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

在C++中,单例模式(Singleton Pattern)是一种常用的设计模式,用来确保一个类只有一个实例,并提供全局访问点。实现线程安全的单例模式是一项挑战,尤其是在多线程环境下,需要保证实例在并发访问时不被多次创建。下面介绍几种常见的实现方式,并比较其优缺点。

1. 经典双重检查锁定(Double-Checked 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;

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

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

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

优点

  • 只在第一次创建实例时获取锁,后续访问无需锁,性能较好。

缺点

  • 需要使用volatilestd::atomic以避免编译器优化导致的可见性问题(C++11之后,std::atomic已解决)。
  • 代码相对繁琐,容易出错。

2. 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起,局部静态变量的初始化是线程安全的,遵循”线程安全的初始化”(call_once内部实现)。

缺点

  • 不能延迟销毁:如果在main函数退出时,单例的析构顺序与其他全局对象可能产生依赖。
  • 对于极度受限的环境(如嵌入式)可能不适合。

3. std::call_oncestd::once_flag

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

    // 需要手动释放
    static void destroy() {
        delete instance_;
        instance_ = nullptr;
    }

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

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

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

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

优点

  • 明确控制实例的创建时机。
  • call_once 只在第一次调用时执行初始化代码,后续调用不再锁。

缺点

  • 需要手动释放内存,或者让实例成为std::unique_ptr,否则会导致内存泄漏。
  • 代码比Meyers略显繁琐。

4. 通过 std::shared_ptr 实现懒加载

class Singleton {
public:
    static std::shared_ptr <Singleton> getInstance() {
        std::call_once(initFlag_, [](){
            instance_ = std::shared_ptr <Singleton>(new Singleton());
        });
        return instance_;
    }

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

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

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

std::shared_ptr <Singleton> Singleton::instance_ = nullptr;
std::once_flag Singleton::initFlag_;

优点

  • 采用智能指针管理生命周期,避免手动删除。
  • 与多线程环境配合良好。

缺点

  • 需要 C++11 标准库支持。
  • 如果全局对象在程序结束时销毁,仍可能出现析构顺序问题。

5. 线程局部存储(Thread-Local Singleton)

如果你想为每个线程提供独立的单例实例,可使用 thread_local

class ThreadSingleton {
public:
    static ThreadSingleton& getInstance() {
        thread_local ThreadSingleton instance;
        return instance;
    }

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

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

优点

  • 每个线程有自己的实例,避免竞争。
  • 初始化与销毁都在线程生命周期内完成。

缺点

  • 不是真正意义上的“全局单例”,如果你需要全局唯一对象,这种方式不合适。

小结

  • 最简洁:Meyers 单例(局部静态变量)是最推荐的实现方式,尤其在 C++11 之后。
  • 可定制:如果你需要更细粒度的初始化控制,std::call_once 是更好的选择。
  • 多线程安全:所有实现都在 C++11 标准库中使用原子操作和锁,确保线程安全。

在实际项目中,建议先尝试使用 Meyers 单例,除非有特殊需求(如需要显式销毁或跨模块的初始化顺序),再考虑其他实现方案。

发表评论