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

在多线程环境下,单例模式需要保证只有一个实例,并且在并发访问时不产生竞争条件。下面介绍几种常见实现方式,并说明各自的优缺点。

1. 经典双重检查锁定(Double-Checked Locking)

class Singleton {
public:
    static Singleton& getInstance() {
        if (!instance_) {                // 第一次检查
            std::lock_guard<std::mutex> lock(mutex_);
            if (!instance_) {            // 第二次检查
                instance_.reset(new Singleton);
            }
        }
        return *instance_;
    }
private:
    Singleton() = default;
    ~Singleton() = default;
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

    static std::unique_ptr <Singleton> instance_;
    static std::mutex mutex_;
};

std::unique_ptr <Singleton> Singleton::instance_ = nullptr;
std::mutex Singleton::mutex_;

优点:延迟初始化,首次访问时才创建实例。
缺点:在 C++11 之前的编译器可能存在指令重排导致线程安全问题;需要两次检查,略微增加代码复杂度。

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

class Singleton {
public:
    static Singleton& getInstance() {
        static Singleton instance;   // 线程安全的局部静态初始化
        return instance;
    }
private:
    Singleton() = default;
    ~Singleton() = default;
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
};

C++11 标准保证局部静态变量在多线程环境下的初始化是线程安全的(所谓的“魔法”)。这段代码简洁易读,且性能优越。

优点:代码最简洁,完全依赖语言实现保证线程安全。
缺点:实例无法延迟销毁(除非在程序结束时),如果需要手动销毁实例,需要额外实现。

3. 使用 std::call_oncestd::once_flag

class Singleton {
public:
    static Singleton& getInstance() {
        std::call_once(flag_, [](){
            instance_ = new Singleton;
        });
        return *instance_;
    }
private:
    Singleton() = default;
    ~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_;

std::call_once 保证闭包只被调用一次,线程安全性好。

优点:对初始化过程的控制更细粒度,可在需要时使用自定义构造函数。
缺点:相比 Meyer’s Singleton 稍显冗长。

4. 结合 std::shared_ptrstd::weak_ptr

class Singleton {
public:
    static std::shared_ptr <Singleton> getInstance() {
        std::lock_guard<std::mutex> lock(mutex_);
        if (auto sp = instance_.lock()) {
            return sp;
        }
        auto sp = std::make_shared <Singleton>();
        instance_ = sp;
        return sp;
    }
private:
    Singleton() = default;
    ~Singleton() = default;
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

    static std::weak_ptr <Singleton> instance_;
    static std::mutex mutex_;
};

std::weak_ptr <Singleton> Singleton::instance_;
std::mutex Singleton::mutex_;

使用 shared_ptr 让单例在没有引用时自动销毁,适用于需要在运行时释放单例的场景。

优点:自动内存管理,线程安全。
缺点:略高的内存开销和运行时开销。

小结

  • Meyer’s Singleton:最推荐,代码最简洁,现代 C++ 编译器已内置线程安全。
  • 双重检查锁定:兼容老旧编译器,但要注意指令重排。
  • std::call_once:适合自定义初始化流程。
  • shared_ptr/weak_ptr:需要单例可销毁时使用。

在实际项目中,除非有特殊需求,一般使用 Meyer’s Singleton 即可满足大多数情况的线程安全单例实现。

发表评论