在现代 C++(C++11 及以后)中,最简洁且线程安全的单例实现依赖于局部静态变量的初始化。C++ 标准保证了函数内部局部静态对象的构造是线程安全的,即使多个线程同时进入该函数,编译器会在内部使用锁机制来确保只构造一次。下面给出一个典型实现,并解释其细节、优点与常见误区。
#include <mutex>
#include <iostream>
class Logger
{
public:
// 公共访问接口
static Logger& instance()
{
static Logger logger; // C++11 保证线程安全
return logger;
}
void log(const std::string& msg)
{
std::lock_guard<std::mutex> lock(mtx_);
std::cout << "[" << ++counter_ << "] " << msg << std::endl;
}
private:
Logger() : counter_(0) {} // 私有构造函数
Logger(const Logger&) = delete;
Logger& operator=(const Logger&) = delete;
std::mutex mtx_;
size_t counter_;
};
关键点解析
-
局部静态对象
static Logger logger;只在第一次调用instance()时创建。后续调用直接返回已存在的实例。由于 C++11 引入了对局部静态对象初始化的线程安全保证,无需额外同步。 -
私有构造与拷贝/赋值删除
通过私有构造函数阻止外部直接创建实例;删除拷贝构造和赋值运算符避免了实例被复制或重新赋值,确保单例唯一。 -
成员同步
虽然实例创建已线程安全,但使用单例的业务方法仍需考虑并发访问。示例中使用std::mutex保护日志输出,避免多线程写入时交错。
常见误区
-
使用
new及手动 delete
传统实现会使用static Logger* ptr = nullptr;并在instance()内进行if (!ptr) ptr = new Logger;。这在多线程环境下可能导致竞争条件,除非手动加锁或使用std::call_once。 -
双重检查锁定(DCL)
在 C++11 前,双重检查锁定常被用来实现懒加载,但需要显式使用std::mutex并对指针进行原子操作。C++11 的局部静态初始化已简化此需求。 -
单例对象的生命周期
局部静态对象在程序终止时会被销毁,若在atexit时使用,需注意对象已经析构的情况。一般不建议在atexit里再次访问单例。
小结
通过利用 C++11 的线程安全局部静态对象初始化,单例实现既简洁又安全。只需确保实例方法的并发访问得到适当同步,即可在多线程程序中安全使用单例。若对性能极度敏感,可考虑使用 std::call_once 或原子指针实现更细粒度的控制,但在大多数场景下,上述实现已足够满足需求。