在多线程环境下实现单例模式最关键的是保证只创建一次实例且不产生竞争条件。下面给出几种常见实现方式,并对其优缺点进行讨论。
1. 基于C++11的函数内部静态对象(Meyers Singleton)
class Singleton {
public:
static Singleton& instance() {
static Singleton instance; // 第一次访问时构造
return instance;
}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() = default;
~Singleton() = default;
};
-
优点
- 简洁易懂,几乎无额外开销。
- C++11标准保证了局部静态对象的线程安全初始化。
- 不需要手动管理内存,避免了内存泄漏。
-
缺点
- 只在首次调用时初始化,之后每次访问都需要同步检查,但这被优化为无锁。
- 如果你想延迟创建实例的时间(比如在特定阶段才需要),需要额外设计。
2. 双重检查锁(Double-Check Locking)
class Singleton {
public:
static Singleton* getInstance() {
if (!instance_) {
std::lock_guard<std::mutex> lock(mutex_);
if (!instance_) {
instance_ = new Singleton();
}
}
return instance_;
}
// 其它成员
private:
Singleton() = default;
static Singleton* instance_;
static std::mutex mutex_;
};
Singleton* Singleton::instance_ = nullptr;
std::mutex Singleton::mutex_;
-
优点
- 延迟实例化,只有在真正需要时才会创建。
- 适用于在旧标准(C++11之前)编写的代码。
-
缺点
- 需要
volatile或std::atomic以保证可见性,避免编译器优化导致的指令重排。 - 实现复杂,容易出现细节错误。
- 在多线程环境下的初始化成本略高于Meyers Singleton。
- 需要
3. 使用 std::call_once 与 std::once_flag
class Singleton {
public:
static Singleton& instance() {
std::call_once(flag_, []() { instance_ = new Singleton(); });
return *instance_;
}
// 其它成员
private:
Singleton() = default;
static Singleton* instance_;
static std::once_flag flag_;
};
Singleton* Singleton::instance_ = nullptr;
std::once_flag Singleton::flag_;
-
优点
- 标准化的单次初始化机制,兼容所有C++11及以后编译器。
- 简单、易读且线程安全。
- 只需一次初始化,后续访问不会再次触发锁。
-
缺点
- 仍然使用裸指针管理实例,需手动销毁(可通过
std::unique_ptr包装)。 - 需要在程序结束前手动销毁或依赖系统内存回收。
- 仍然使用裸指针管理实例,需手动销毁(可通过
4. 延迟加载的线程安全单例(带销毁)
如果你需要在程序结束时手动销毁实例,可以使用 std::unique_ptr 与 std::weak_ptr 结合:
class Singleton {
public:
static std::shared_ptr <Singleton> getInstance() {
std::call_once(flag_, [](){
instance_.reset(new Singleton());
});
return instance_;
}
private:
Singleton() = default;
static std::unique_ptr <Singleton> instance_;
static std::once_flag flag_;
};
std::unique_ptr <Singleton> Singleton::instance_;
std::once_flag Singleton::flag_;
- 通过
std::shared_ptr的引用计数可以实现多线程安全的访问与自动销毁。 - 需要注意的是
unique_ptr与shared_ptr的转换成本和引用计数开销。
小结
- 最推荐:C++11及以后版本,使用 Meyers Singleton(局部静态对象)即可满足大多数需求,代码最简洁、性能最好。
- 若需要 显式控制实例生命周期 或 兼容旧标准,可选择
std::call_once或 双重检查锁。 - 在任何实现中,禁止拷贝构造和赋值,并在必要时使用
std::unique_ptr或std::shared_ptr进行资源管理。
通过上述方法,你可以在C++中安全、高效地实现单例模式,为多线程应用提供可靠的共享资源管理。