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

在多线程环境下,单例模式的实现需要保证:

  1. 只创建一次实例;
  2. 在实例创建期间不会出现竞争条件;
  3. 同时保持高性能,不给每一次访问都加锁。

以下是几种常见的实现方式,并分别讨论它们的优缺点。

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

class Singleton {
public:
    static Singleton& instance() {
        static Singleton obj;   // C++11 之后编译器保证线程安全
        return obj;
    }
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
private:
    Singleton() = default;
};

说明

  • 线程安全:C++11 标准规定局部静态变量在第一次使用时的初始化是线程安全的。
  • 懒加载:实例只有在 instance() 第一次被调用时才创建。
  • 实现简单:无须显式锁。

缺点

  • 无法在编译时控制实例化时间:如果想在程序启动前就实例化,需要显式调用 instance()
  • 不可销毁:对象会在程序退出时按自然顺序析构,若有依赖顺序的析构需求需要额外处理。

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

class Singleton {
public:
    static Singleton* instance() {
        if (ptr_ == nullptr) {                     // 第一重检查
            std::lock_guard<std::mutex> lock(mutex_);
            if (ptr_ == nullptr) {                 // 第二重检查
                ptr_ = new Singleton();
            }
        }
        return ptr_;
    }
    ~Singleton() { delete ptr_; }

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

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

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

说明

  • 懒加载 + 手动控制:在需要时才创建,且可以决定何时销毁。
  • 多线程安全:使用互斥锁保证唯一性。

缺点

  • 性能开销:第一次实例化时需要加锁,且在每次访问时都会进行两次空指针检查。
  • 实现细节:需要正确使用 volatilestd::atomic,否则可能出现指令重排导致的“半初始化”对象。

3. std::call_once + std::once_flag

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

private:
    Singleton() = default;
    static Singleton* instancePtr_;
    static std::once_flag initFlag_;
};

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

说明

  • 一次性初始化std::call_once 保证闭包只执行一次,无论有多少线程并发访问。
  • 线程安全且性能更佳:相比双重检查锁,std::call_once 的实现通常更高效。

缺点

  • 同样是手动销毁:需要在适当的时机手动删除实例,否则会造成内存泄漏。

4. 基于 C++17 的 inline 变量

class Singleton {
public:
    static Singleton& instance() {
        static Singleton obj;
        return obj;
    }
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
private:
    Singleton() = default;
};

inline Singleton& getSingleton() {
    return Singleton::instance();
}

说明

  • inline 变量在 C++17 后允许在头文件中定义,避免多定义错误。
  • 与 Meyer’s 方案等价,只是更明确表达实现细节。

5. 线程安全的懒加载 + 对象销毁顺序

如果单例依赖其他全局对象,需要控制销毁顺序,可以使用 std::unique_ptr 并配合 std::atexit

class Singleton {
public:
    static Singleton& instance() {
        static std::unique_ptr <Singleton> ptr;
        if (!ptr) {
            ptr.reset(new Singleton());
            std::atexit([](){ ptr.reset(); }); // 程序结束时销毁
        }
        return *ptr;
    }
    // ...
};

总结

  • 最推荐:使用 Meyer’s Singleton(局部静态变量),因其实现简单且符合 C++11 标准的线程安全保证。
  • 特殊需求:若需要手动销毁或在编译期确定实例化时间,std::call_once 或双重检查锁是更灵活的选择。
  • 注意:在任何实现中都要删除拷贝构造和赋值操作,避免被错误复制。

通过选择合适的实现方式,可以在多线程环境中安全、高效地使用单例模式。

发表评论