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

在多线程环境下,单例模式的实现不仅要保证全局唯一性,还必须防止竞争条件导致多实例创建。下面分别介绍几种常见的实现方式,比较它们的优缺点,并给出可直接使用的代码示例。

1. 基于局部静态变量(Meyer’s Singleton)

C++11 之后,函数内部的局部静态变量初始化是线程安全的。最简单、最推荐的实现方式:

class Singleton {
public:
    static Singleton& instance() {
        static Singleton instance;   // 线程安全的初始化
        return instance;
    }

    // 禁止拷贝和移动
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
    Singleton(Singleton&&) = delete;
    Singleton& operator=(Singleton&&) = delete;

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

优点:

  • 代码简洁,几乎没有任何锁开销。
  • 只在第一次调用时创建,随后直接返回。

缺点:

  • 对象在程序结束时才销毁,若在程序退出时使用可能导致析构顺序问题(但大多数情况下可以忽略)。

2. std::call_oncestd::once_flag

如果你想更明确地控制初始化过程,可以使用 std::call_once

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

    ~Singleton() { delete instancePtr; }

private:
    Singleton() = default;
    static Singleton* instancePtr;
    static std::once_flag initFlag;
};

Singleton* Singleton::instancePtr = nullptr;
std::once_flag Singleton::initFlag;

优点:

  • 可以在初始化时执行更复杂的逻辑(如读取配置文件)。
  • Meyer's 实现一样,线程安全。

缺点:

  • 需要手动管理单例的销毁(delete),否则可能出现内存泄漏。

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

老式的做法,早期 C++ 标准中不保证线程安全,直到 C++11 的内存模型才可靠:

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

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

std::atomic<Singleton*> Singleton::instancePtr{nullptr};
std::mutex Singleton::mtx;

优点:

  • 仅在第一次创建时使用锁,之后访问无需锁。

缺点:

  • 代码复杂,容易出现错误。
  • 需要严格遵守 C++11 的内存顺序规则。

4. 线程安全的懒加载(使用 std::shared_ptr

如果你需要在单例中维护可变资源,并且想自动管理其生命周期,可以用 std::shared_ptr

class Singleton {
public:
    static std::shared_ptr <Singleton> instance() {
        std::call_once(initFlag, []() {
            ptr = std::make_shared <Singleton>();
        });
        return ptr;
    }

private:
    Singleton() = default;
    static std::shared_ptr <Singleton> ptr;
    static std::once_flag initFlag;
};

std::shared_ptr <Singleton> Singleton::ptr;
std::once_flag Singleton::initFlag;

优点:

  • 自动析构,适合需要共享生命周期的场景。
  • 对多线程读访问没有额外成本。

缺点:

  • 每次访问返回一个 shared_ptr,虽然轻量,但仍有引用计数开销。

5. 何时选择哪种实现?

实现方式 代码简洁 初始化成本 资源释放 适用场景
Meyer’s Singleton 仅一次 程序退出时 最常见、最简单
std::call_once 仅一次 手动或显式释放 需要自定义初始化
Double-Checked Locking 仅一次 手动 旧代码维护
std::shared_ptr 仅一次 自动 资源共享、可变生命周期

在绝大多数现代 C++ 项目中,Meyer’s Singleton 是首选。它几乎无锁、易于使用,并且符合 C++11 之后的线程安全保证。除非你有特殊需求(如自定义析构、动态资源加载),否则不必使用更复杂的方案。

6. 小结

  • 单例模式在多线程中实现时,核心是保证初始化阶段的线程安全。
  • C++11 引入的局部静态变量和 std::call_once 提供了最简洁且安全的实现方式。
  • 传统的双重检查锁虽然可行,但更难维护,建议只在极其特殊的性能需求下使用。
  • 记得处理好单例的析构顺序,避免在 atexit 时出现依赖冲突。

通过以上方法,你可以在任何多线程 C++ 项目中安全、可靠地实现单例模式。

发表评论