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

在多线程环境下,保证单例对象的线程安全性是一个常见的挑战。C++11 引入了线程安全的静态局部变量初始化,极大地方便了单例实现。下面将从三个角度展开讨论:Meyers 单例、双检锁(Double-Checked Locking)以及基于 std::call_once 的实现。


1. Meyers 单例(线程安全的静态局部变量)

class Logger {
public:
    static Logger& getInstance() {
        static Logger instance;   // C++11 规定线程安全的初始化
        return instance;
    }

    void log(const std::string& msg) {
        std::lock_guard<std::mutex> lock(mtx_);
        std::cout << msg << std::endl;
    }

private:
    Logger() = default;
    Logger(const Logger&) = delete;
    Logger& operator=(const Logger&) = delete;

    std::mutex mtx_;
};

优点

  • 代码简洁,完全利用语言特性。
  • 无需显式锁或同步机制,避免死锁与性能问题。

缺点

  • 仅在 C++11 之后可用。
  • 不能在实例销毁前做自定义操作(除非手动实现 std::unique_ptrstd::shared_ptr)。

2. 双检锁(Double-Checked Locking)

在 C++11 之前,双检锁是实现线程安全单例的常用手段。由于编译器优化和 CPU 指令重排,传统实现可能出现数据竞争。使用 std::atomic 可以确保可见性。

#include <atomic>
#include <mutex>

class Config {
public:
    static Config* getInstance() {
        Config* 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 Config();
                instance_.store(tmp, std::memory_order_release);
            }
        }
        return tmp;
    }

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

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

优点

  • 兼容旧标准(C++03)。
  • 对象初始化延迟,首次调用时才创建。

缺点

  • 代码更繁琐,易出错。
  • 需要手动管理内存,易导致内存泄漏。

3. 基于 std::call_once 的实现

C++11 标准库提供 std::call_once,可以让你只调用一次某个函数,天然线程安全。

#include <mutex>

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

private:
    Service() = default;
    static Service* instance_;
    static std::once_flag initFlag_;
};

Service* Service::instance_ = nullptr;
std::once_flag Service::initFlag_;

优点

  • 代码简洁,易维护。
  • 对象销毁时仍然可以自定义析构顺序(如 std::atexit)。

缺点

  • 仍需手动处理内存(除非使用 std::unique_ptr)。
  • Meyers 单例相比,略有性能开销(调用一次 std::call_once 的成本)。

选型建议

实现方式 适用场景 主要优势 主要劣势
Meyers 单例 C++11+ 简洁、性能优越 无法在销毁前自定义
双检锁 C++03 兼容旧标准 复杂、易出错
std::call_once C++11+ 灵活、线程安全 需手动内存管理

在现代 C++ 项目中,推荐使用 Meyers 单例std::call_once 结合 std::unique_ptr,既保证线程安全,又避免手动内存管理的风险。


结语

单例模式本质上是“唯一实例”的实现,真正需要关注的往往不是单例本身,而是 线程安全性初始化时机资源释放。掌握 C++11 及以后的特性后,单例的实现可以做到既安全又简洁。希望本文能帮助你在项目中做出合适的选择。

发表评论