如何在C++中实现线程安全的单例模式(双重检查锁)

在多线程环境中,单例模式需要确保只创建一次实例且线程安全。下面介绍在C++17/20中实现双重检查锁(Double-Checked Locking)的一种可靠方式,并解释其细节。

1. 经典实现的缺陷

传统的双重检查锁实现是:

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

在C++98/03中,这种实现存在内存屏障和编译器重排序问题:new Singleton() 的写入可能在 ptr 的赋值之前被重排序,导致其他线程看到未初始化的对象。

2. C++ 的内存模型解决方案

C++11 起,std::atomicstd::memory_order 提供了对内存顺序的精确控制。可以改用 std::atomic<Singleton*> 并使用 memory_order_acquire/release

#include <atomic>
#include <mutex>

class Singleton {
public:
    static Singleton& instance() {
        Singleton* tmp = instance_.load(std::memory_order_acquire);
        if (!tmp) {                            // 第一次检查
            std::lock_guard<std::mutex> lock(mtx_);
            tmp = instance_.load(std::memory_order_relaxed);
            if (!tmp) {                        // 第二次检查
                tmp = new Singleton();
                instance_.store(tmp, std::memory_order_release);
            }
        }
        return *tmp;
    }

private:
    Singleton() = default;
    static std::atomic<Singleton*> instance_;
    static std::mutex mtx_;
};

std::atomic<Singleton*> Singleton::instance_{nullptr};
std::mutex Singleton::mtx_;

关键点解释

  • instance_ 是原子指针,保证对它的读写是原子操作。
  • load(std::memory_order_acquire):当读取到非空指针时,后续所有操作必须在此之前完成。
  • store(std::memory_order_release):写入指针时,前面的所有操作必须在此之前完成。
  • std::memory_order_relaxed 用于在加锁内部的再次检查,因为此时锁已保证原子性。

3. 更简洁的 C++17 方案

C++17 的 std::call_oncestd::once_flag 本身就是线程安全的单例初始化工具。

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

private:
    Singleton() = default;
    static Singleton* instance_;
    static std::once_flag flag_;
};

Singleton* Singleton::instance_ = nullptr;
std::once_flag Singleton::flag_;
  • call_once 确保 lambda 只被执行一次,且对所有线程可见。
  • 该实现避免了手动使用 std::atomic,更易读、易维护。

4. 对象销毁

在多线程程序结束时,单例对象可能需要被销毁。

  • 采用 std::unique_ptr 或者 std::shared_ptr 并配合 std::call_once 进行销毁。
  • 或者利用程序退出时的静态对象析构顺序(在 main 结束后自动销毁)。
class Singleton {
public:
    static Singleton& instance() {
        std::call_once(flag_, []{
            instance_.reset(new Singleton());
        });
        return *instance_;
    }

private:
    Singleton() = default;
    static std::unique_ptr <Singleton> instance_;
    static std::once_flag flag_;
};

std::unique_ptr <Singleton> Singleton::instance_;
std::once_flag Singleton::flag_;

5. 小结

  • 对于 C++11+,建议使用 std::atomic + memory_orderstd::call_once,两者都能安全实现单例。
  • std::call_once 更易读,且内部已处理所有同步细节,适合大多数场景。
  • 若需延迟初始化且不想使用 std::call_once,可使用 std::atomic 并严格控制内存顺序。

通过上述方法,你可以在 C++ 中实现既安全又高效的线程安全单例。

发表评论