**如何在C++中实现一个线程安全的懒加载单例模式?**

在多线程环境下实现线程安全的懒加载单例是一个常见的需求。下面将演示几种常见的方法,并说明它们的优缺点。


1. 基于 std::call_once 的实现

#include <mutex>

class Singleton {
public:
    static Singleton& instance() {
        std::call_once(initFlag, [](){
            instancePtr = new Singleton();
        });
        return *instancePtr;
    }

    // 禁止复制和移动
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
    Singleton(Singleton&&) = delete;
    Singleton& operator=(Singleton&&) = delete;

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

    static Singleton* instancePtr;
    static std::once_flag initFlag;
};

Singleton* Singleton::instancePtr = nullptr;
std::once_flag Singleton::initFlag;

优点

  • 代码简洁,易于理解。
  • std::call_once 已在标准库中实现,经过充分测试。
  • 确保一次且仅一次初始化,无论多少线程访问。

缺点

  • 需要手动管理单例生命周期(如在程序结束时手动删除,或使用智能指针)。
  • 仅适用于 C++11 及以上。

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

#include <mutex>

class Singleton {
public:
    static Singleton& instance() {
        if (instancePtr == nullptr) {
            std::lock_guard<std::mutex> lock(mtx);
            if (instancePtr == nullptr) {
                instancePtr = new Singleton();
            }
        }
        return *instancePtr;
    }

    // 复制/移动禁用
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

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

    static Singleton* instancePtr;
    static std::mutex mtx;
};

Singleton* Singleton::instancePtr = nullptr;
std::mutex Singleton::mtx;

优点

  • 只在第一次访问时加锁,后续访问不需要加锁,性能较好。

缺点

  • 需要保证内存模型中的可见性;在旧编译器或弱内存模型下可能产生未定义行为。
  • 代码更复杂,容易出错。

3. 使用 std::shared_ptr + std::once_flag

如果你希望单例在程序结束时自动销毁,可结合 std::shared_ptr

#include <memory>
#include <mutex>

class Singleton {
public:
    static std::shared_ptr <Singleton> instance() {
        std::call_once(initFlag, [](){
            instancePtr = std::shared_ptr <Singleton>(new Singleton());
        });
        return instancePtr;
    }

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

    static std::shared_ptr <Singleton> instancePtr;
    static std::once_flag initFlag;
};

std::shared_ptr <Singleton> Singleton::instancePtr = nullptr;
std::once_flag Singleton::initFlag;

优点

  • 自动管理内存,程序结束时自动析构。
  • std::call_once 的结合保证了线程安全。

缺点

  • 需要注意 shared_ptr 的循环引用问题(如果单例中持有自身引用)。

4. C++17 的 inline static 变量

自 C++17 起,类内声明 inline static 变量可以在头文件中定义,且在每个翻译单元中仅产生一次实例:

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;
    Singleton(Singleton&&) = delete;
    Singleton& operator=(Singleton&&) = delete;
};

优点

  • 极其简洁,利用了 C++11 的“局部静态变量初始化线程安全”保证。
  • 不需要手动销毁,生命周期由程序结束决定。

缺点

  • 只能在 C++11 及以上使用。
  • 需要在 instance() 内部使用局部静态变量,若类构造函数有复杂逻辑,可能导致初始化顺序问题。

5. 小结

  • std::call_once + once_flag:最安全、最易维护的方式,适合大多数场景。
  • 双重检查锁定:性能略好,但实现复杂,需要注意内存模型。
  • std::shared_ptr:适合需要自动销毁的场景,避免手动 delete
  • C++17 的 inline static:最简洁,利用编译器保证线程安全。

在实际项目中,推荐使用 std::call_once,因为它既简洁又安全;若你正在使用 C++17 并且不需要在单例中管理复杂资源,可以直接使用 inline static 的方式。


发表评论