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

在多线程环境下,单例模式的实现尤为重要。下面将展示一种利用C++11特性实现线程安全单例的经典方案,并讨论其优缺点。

1. 经典懒汉式实现(线程安全)

class Singleton {
public:
    // 获取单例实例
    static Singleton& getInstance() {
        static Singleton instance;   // C++11 保证局部静态对象初始化线程安全
        return instance;
    }

    // 删除拷贝构造和赋值操作,确保真正是单例
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

    void doSomething() {
        // 业务逻辑
    }

private:
    Singleton() = default;          // 私有构造函数
    ~Singleton() = default;         // 私有析构函数
};

关键点解析

  1. 局部静态变量

    • static Singleton instance; 位于 getInstance() 内部。C++11 起,编译器会保证此对象在第一次访问时线程安全地完成构造。
    • 不需要手动加锁,代码简洁。
  2. 删除拷贝/移动构造

    • 防止外部复制或移动导致多实例。
  3. 懒加载

    • 只有第一次调用 getInstance() 时才创建实例,节省资源。

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

在 C++11 之前的代码库中,常见的做法是使用双重检查锁来实现线程安全的单例:

class Singleton {
public:
    static Singleton* getInstance() {
        if (instance_ == nullptr) {
            std::lock_guard<std::mutex> lock(mutex_);
            if (instance_ == nullptr) {
                instance_ = new Singleton();
            }
        }
        return instance_;
    }

    // 其他成员...

private:
    Singleton() = default;
    ~Singleton() = default;
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

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

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

缺陷

  • 内存屏障与可见性:在某些平台上,instance_ 的更新可能不会立即在其他线程可见,导致“野指针”。
  • 额外锁开销:即使实例已创建,每次访问都要尝试获取锁,虽然大多数实现使用了“只读”锁来优化,但仍然多了一层不必要的开销。

3. 只读锁的优化

如果你需要在频繁读取单例的环境下减少锁竞争,可以使用读写锁(如 std::shared_mutex):

#include <shared_mutex>

class Singleton {
public:
    static Singleton& getInstance() {
        std::shared_lock<std::shared_mutex> readLock(mutex_);
        if (!instance_) {
            readLock.unlock();
            std::unique_lock<std::shared_mutex> writeLock(mutex_);
            if (!instance_) {
                instance_ = std::make_unique <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::shared_mutex mutex_;
};

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

4. 何时使用哪种实现?

场景 推荐实现 说明
需要最少代码量,且编译器支持 C++11 懒汉式局部静态 简洁、线程安全
需要懒加载并且想显式控制锁 双重检查锁 兼容老编译器
需要在高并发读场景下减少锁竞争 读写锁 + 双重检查 适用于读多写少的单例

5. 小结

  • C++11 的局部静态变量提供了最简单、最安全的单例实现。
  • 传统双重检查锁虽兼容老版本,但存在可见性与锁开销问题。
  • 读写锁可在特定读多写少的场景下进一步优化。

在实际项目中,建议优先使用 C++11 的局部静态实现,除非有特殊性能需求或兼容性限制。

发表评论