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

在多线程环境下,单例模式的实现需要考虑并发访问导致的竞争条件。下面通过几种常见的实现方式来演示如何在C++中实现线程安全的单例模式,并分析各自的优缺点。

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

#include <mutex>

class Singleton {
public:
    static Singleton& instance() {
        if (!ptr_) {               // 第一检查(非锁)
            std::lock_guard<std::mutex> lock(mutex_);
            if (!ptr_) {           // 第二检查(加锁后)
                ptr_ = new Singleton();
            }
        }
        return *ptr_;
    }

    // 复制构造与赋值运算符禁止
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

private:
    Singleton() {}
    ~Singleton() { delete ptr_; }

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

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

优点

  • 只在第一次初始化时加锁,性能相对较好。

缺点

  • 需要对指针进行原子操作或使用 std::atomic<Singleton*>,否则在某些编译器或架构下可能出现指令重排序导致的可见性问题。
  • 代码略显复杂。

2. C++11 的局部静态变量(Meyer’s Singleton)

class Singleton {
public:
    static Singleton& instance() {
        static Singleton instance;  // C++11 规定线程安全的局部静态初始化
        return instance;
    }

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

private:
    Singleton() {}
    ~Singleton() {}
};

优点

  • 代码简洁,完全符合 C++11 标准,编译器保证线程安全。
  • 自动在程序结束时销毁。

缺点

  • 对于需要在程序早期初始化(例如在 main 之前)的情况,可能会出现“使用前未初始化”问题,尽管可以通过提前调用 instance() 解决。
  • 需要 C++11 或更高版本。

3. 显式锁和一次性初始化(std::call_once)

#include <mutex>

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

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

private:
    Singleton() {}
    ~Singleton() { delete ptr_; }

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

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

优点

  • std::call_once 保证初始化只执行一次,线程安全且性能良好。
  • Meyer's Singleton 相比,可在任何线程中安全调用。

缺点

  • 需要手动管理指针,容易出现内存泄漏。
  • 对于全局对象销毁顺序不确定,可能导致访问已被销毁的单例。

4. 线程安全的懒加载与销毁(带计数器)

#include <atomic>

class Singleton {
public:
    static Singleton& instance() {
        Singleton* tmp = ptr_.load(std::memory_order_acquire);
        if (!tmp) {
            std::lock_guard<std::mutex> lock(mutex_);
            tmp = ptr_.load(std::memory_order_relaxed);
            if (!tmp) {
                tmp = new Singleton();
                ptr_.store(tmp, std::memory_order_release);
            }
        }
        ++refCount_;
        return *tmp;
    }

    static void release() {
        if (--refCount_ == 0) {
            std::lock_guard<std::mutex> lock(mutex_);
            if (refCount_ == 0) {
                delete ptr_;
                ptr_ = nullptr;
            }
        }
    }

private:
    Singleton() {}
    ~Singleton() {}

    static std::atomic<Singleton*> ptr_;
    static std::atomic <int> refCount_;
    static std::mutex mutex_;
};

std::atomic<Singleton*> Singleton::ptr_{nullptr};
std::atomic <int> Singleton::refCount_{0};
std::mutex Singleton::mutex_;

优点

  • 支持懒加载、可在多次 instance() 调用后手动销毁,避免全局对象销毁顺序问题。

缺点

  • 代码较为复杂,需要手动调用 release()
  • 计数错误会导致内存泄漏或提前销毁。

5. 现代 C++ 的 std::shared_ptr

#include <memory>
#include <mutex>

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

private:
    Singleton() {}
    ~Singleton() {}

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

std::shared_ptr <Singleton> Singleton::ptr_;
std::once_flag Singleton::initFlag_;

优点

  • 使用智能指针自动管理生命周期。
  • call_once 保证线程安全。

缺点

  • 需要在所有使用者中保持 std::shared_ptr 的引用,若未保持会导致单例被销毁,后续访问会崩溃。

小结

  • Meyer’s Singleton:最简洁、最安全,推荐在 C++11 及以上使用。
  • std::call_once + std::unique_ptr:在需要手动销毁或需要更细粒度控制时使用。
  • 双重检查锁定:兼容旧编译器,但更容易出错,建议避免。
  • 计数器实现:适合需要在运行时动态销毁单例的高级场景。

在实际项目中,建议先评估单例的生命周期、使用场景以及对线程安全的严格程度,选择最符合需求的实现方式。祝编码愉快!

发表评论