**如何在 C++ 中实现一个线程安全的懒汉式单例?**

在多线程环境下,单例模式的实现往往会遇到并发安全问题。下面以 C++17 为例,介绍几种常用且线程安全的实现方式,并说明各自的优缺点。


1. Meyers 单例(局部静态变量)

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;
};
  • 优点

    • 代码简洁,编译器负责初始化顺序。
    • C++11 引入的局部静态变量初始化是线程安全的,避免了显式锁。
  • 缺点

    • 延迟初始化:如果程序未访问 instance(),对象永不创建。
    • 对于类构造失败时的异常处理,只有在第一次访问时才会抛出。

2. 双重检查锁(Double-Checked Locking)

class Singleton {
public:
    static Singleton* getInstance() {
        Singleton* tmp = instance_.load(std::memory_order_acquire);
        if (!tmp) {
            std::lock_guard<std::mutex> lock(mutex_);
            tmp = instance_.load(std::memory_order_relaxed);
            if (!tmp) {
                tmp = new Singleton();
                instance_.store(tmp, std::memory_order_release);
            }
        }
        return tmp;
    }
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
private:
    Singleton() = default;
    static std::atomic<Singleton*> instance_;
    static std::mutex mutex_;
};

std::atomic<Singleton*> Singleton::instance_{nullptr};
std::mutex Singleton::mutex_;
  • 优点

    • 仅在第一次创建时使用锁,后续访问无锁开销。
    • 适用于需要手动销毁或在全局析构顺序中控制的情况。
  • 缺点

    • 代码复杂,容易出现细微错误(如内存序问题)。
    • 在 C++11 之前,双重检查锁并不保证线程安全。

3. 经典的 std::call_once

class Singleton {
public:
    static Singleton& instance() {
        std::call_once(flag_, []() { ptr_ = new Singleton(); });
        return *ptr_;
    }
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
private:
    Singleton() = default;
    static Singleton* ptr_;
    static std::once_flag flag_;
};

Singleton* Singleton::ptr_ = nullptr;
std::once_flag Singleton::flag_;
  • 优点

    • 代码可读性好,内部使用 std::once_flag 保障一次性执行。
    • 适合需要显式销毁或在某些平台上控制初始化顺序的情况。
  • 缺点

    • 仍然需要手动管理内存,若不销毁会造成内存泄漏。
    • 对比 Meyers 单例,稍有性能损耗(一次锁判断)。

4. std::shared_ptr 结合 std::weak_ptr

class Singleton {
public:
    static std::shared_ptr <Singleton> instance() {
        std::lock_guard<std::mutex> lock(mutex_);
        if (!ptr_) {
            ptr_ = std::shared_ptr <Singleton>(new Singleton(), [](Singleton*){});
        }
        return ptr_;
    }
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
private:
    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 可以在需要时检测实例是否已被销毁。
    • 自动内存管理,避免手动 delete
  • 缺点

    • 仍然需要显式锁。
    • weak_ptr 的使用稍显冗余,除非需要监控实例生命周期。

小结

  • 最推荐:Meyers 单例(局部静态变量)——最简洁、线程安全、几乎无性能损耗。
  • 需要显式销毁std::call_oncestd::shared_ptr/std::weak_ptr 方案。
  • 高性能需求:双重检查锁,但必须确保正确使用原子和内存序。

在实际项目中,除非你有特殊需求(如需要在多线程程序退出前手动销毁单例),否则建议使用第一种方法,以保持代码的简洁与安全。

发表评论