在多线程环境下实现线程安全的懒加载单例是一个常见的需求。下面将演示几种常见的方法,并说明它们的优缺点。
1. 基于 std::call_once 的实现
#include <mutex>
class Singleton {
public:
static Singleton& instance() {
std::call_once(initFlag, [](){
instancePtr = new Singleton();
});
return *instancePtr;
}
// 禁止复制和移动
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
Singleton(Singleton&&) = delete;
Singleton& operator=(Singleton&&) = delete;
private:
Singleton() = default;
~Singleton() = default;
static Singleton* instancePtr;
static std::once_flag initFlag;
};
Singleton* Singleton::instancePtr = nullptr;
std::once_flag Singleton::initFlag;
优点
- 代码简洁,易于理解。
std::call_once已在标准库中实现,经过充分测试。- 确保一次且仅一次初始化,无论多少线程访问。
缺点
- 需要手动管理单例生命周期(如在程序结束时手动删除,或使用智能指针)。
- 仅适用于 C++11 及以上。
2. 双重检查锁定(Double-Check Locking)
#include <mutex>
class Singleton {
public:
static Singleton& instance() {
if (instancePtr == nullptr) {
std::lock_guard<std::mutex> lock(mtx);
if (instancePtr == nullptr) {
instancePtr = new Singleton();
}
}
return *instancePtr;
}
// 复制/移动禁用
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() = default;
~Singleton() = default;
static Singleton* instancePtr;
static std::mutex mtx;
};
Singleton* Singleton::instancePtr = nullptr;
std::mutex Singleton::mtx;
优点
- 只在第一次访问时加锁,后续访问不需要加锁,性能较好。
缺点
- 需要保证内存模型中的可见性;在旧编译器或弱内存模型下可能产生未定义行为。
- 代码更复杂,容易出错。
3. 使用 std::shared_ptr + std::once_flag
如果你希望单例在程序结束时自动销毁,可结合 std::shared_ptr:
#include <memory>
#include <mutex>
class Singleton {
public:
static std::shared_ptr <Singleton> instance() {
std::call_once(initFlag, [](){
instancePtr = std::shared_ptr <Singleton>(new Singleton());
});
return instancePtr;
}
private:
Singleton() = default;
~Singleton() = default;
static std::shared_ptr <Singleton> instancePtr;
static std::once_flag initFlag;
};
std::shared_ptr <Singleton> Singleton::instancePtr = nullptr;
std::once_flag Singleton::initFlag;
优点
- 自动管理内存,程序结束时自动析构。
- 与
std::call_once的结合保证了线程安全。
缺点
- 需要注意
shared_ptr的循环引用问题(如果单例中持有自身引用)。
4. C++17 的 inline static 变量
自 C++17 起,类内声明 inline static 变量可以在头文件中定义,且在每个翻译单元中仅产生一次实例:
class Singleton {
public:
static Singleton& instance() {
static Singleton instance; // C++11 之后线程安全
return instance;
}
private:
Singleton() = default;
~Singleton() = default;
// 禁止复制/移动
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
Singleton(Singleton&&) = delete;
Singleton& operator=(Singleton&&) = delete;
};
优点
- 极其简洁,利用了 C++11 的“局部静态变量初始化线程安全”保证。
- 不需要手动销毁,生命周期由程序结束决定。
缺点
- 只能在 C++11 及以上使用。
- 需要在
instance()内部使用局部静态变量,若类构造函数有复杂逻辑,可能导致初始化顺序问题。
5. 小结
std::call_once+once_flag:最安全、最易维护的方式,适合大多数场景。- 双重检查锁定:性能略好,但实现复杂,需要注意内存模型。
std::shared_ptr:适合需要自动销毁的场景,避免手动delete。- C++17 的
inline static:最简洁,利用编译器保证线程安全。
在实际项目中,推荐使用 std::call_once,因为它既简洁又安全;若你正在使用 C++17 并且不需要在单例中管理复杂资源,可以直接使用 inline static 的方式。