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

在 C++ 中实现单例模式是常见的设计模式之一,目的是让一个类只有一个实例并提供全局访问点。然而,当程序进入多线程环境时,单例的创建过程必须是线程安全的,否则可能导致多个实例被创建,或者出现竞争条件。下面将从几个角度讨论并演示如何在 C++ 中实现线程安全的单例模式。


1. 单例模式的基本实现

class Singleton {
public:
    static Singleton& instance() {
        static Singleton instance;   // C++11 之后的线程安全
        return instance;
    }

    // 禁止复制与赋值
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

private:
    Singleton() = default;
    ~Singleton() = default;
};
  • 局部静态对象static Singleton instance; 在第一次调用 instance() 时创建,随后每次调用直接返回已创建对象。
  • C++11 标准:保证局部静态对象的初始化是线程安全的。即使多个线程同时调用 instance(),编译器会在内部加锁,确保只会创建一次对象。

2. 传统的双重检查锁(DCL)实现

在 C++11 之前,常用的线程安全单例实现是双重检查锁:

#include <mutex>

class Singleton {
public:
    static Singleton* instance() {
        if (!instance_) {                // 第一次检查
            std::lock_guard<std::mutex> lock(mutex_);
            if (!instance_) {            // 第二次检查
                instance_ = new Singleton();
            }
        }
        return instance_;
    }

    // 同样禁用复制与赋值
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

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

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

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

优点

  • 只在第一次需要实例时才加锁,后续访问性能高。

缺点

  • 需要手动管理对象生命周期,容易出现内存泄漏或早期销毁。
  • 在某些编译器或优化策略下,可能仍存在指令重排导致的线程安全问题(需使用 std::atomic 或内存屏障)。

3. 用 std::call_once 实现单例

std::call_once 提供了一种更简洁且安全的方式来保证一次性初始化。

#include <mutex>

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

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

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

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

Singleton* Singleton::instance_ = nullptr;
std::once_flag Singleton::initFlag_;
  • std::once_flag 只保证第一次调用 call_once 的 lambda 函数被执行一次。
  • 与 DCL 相比,call_once 更易读且不需要手动锁。

4. 基于 std::shared_ptr 的单例

如果需要在单例被销毁后还能重新创建,可以使用 std::shared_ptr 并结合 std::weak_ptr

#include <memory>
#include <mutex>

class Singleton {
public:
    static std::shared_ptr <Singleton> instance() {
        std::lock_guard<std::mutex> lock(mutex_);
        if (auto sp = ptr_.lock()) {
            return sp;                  // 已有实例,直接返回
        }
        auto sp = std::shared_ptr <Singleton>(new Singleton());
        ptr_ = sp;                      // 记录弱指针
        return sp;
    }

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

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

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

std::weak_ptr <Singleton> Singleton::ptr_;
std::mutex Singleton::mutex_;
  • 通过 std::weak_ptr 检查实例是否已经存在。
  • 如果所有 std::shared_ptr 实例都销毁,单例会被释放,随后再次调用 instance() 可以重新创建。

5. 性能与实现细节对比

实现方式 线程安全 代码复杂度 性能 适用场景
局部静态对象(C++11+) 需要单例在整个程序生命周期内存在
双重检查锁(DCL) ✔(需注意指令重排) 老项目、无法使用 C++11
std::call_once C++11 及以后,想显式控制初始化
std::shared_ptr+std::weak_ptr 需要可被销毁并重建的单例

6. 常见错误与注意事项

  1. 复制构造/赋值:一定要显式删除,否则外部可以复制单例实例,导致出现多个实例。
  2. 对象销毁顺序:如果使用局部静态对象,销毁顺序不确定,可能导致在析构过程中访问已销毁的静态对象。
  3. 指针悬挂:使用裸指针时要注意生命周期,避免在析构时访问已释放内存。
  4. 全局变量优先:C++ 运行时全局变量的销毁顺序与线程的结束顺序无关,需谨慎使用全局单例。

7. 小结

  • 在 C++11 及以后,推荐使用局部静态对象std::call_once来实现单例,既简洁又线程安全。
  • 如果需要单例可销毁并在之后重新创建,可以使用std::shared_ptrstd::weak_ptr组合。
  • 对于老项目或特殊需求,双重检查锁仍是可行方案,但需额外关注指令重排与内存屏障。

掌握这些实现技巧后,你就能在任何 C++ 项目中安全、可靠地使用单例模式,既满足全局访问的需求,又兼顾多线程环境的稳定性。

发表评论