在 C++ 中实现单例模式时,线程安全是一个关键考虑因素。下面介绍几种常见且安全的实现方式,并比较它们的优缺点。
1. Meyers 单例(局部静态变量)
class Singleton {
public:
static Singleton& instance() {
static Singleton instance; // C++11 之后是线程安全的
return instance;
}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() = default;
};
- 优点:代码简洁,编译器保证初始化是线程安全的(自 C++11 起)。无额外同步开销。
- 缺点:无法在构造期间抛出异常(如果构造抛异常,后续调用仍会重试,可能导致程序进入不确定状态)。如果需要在程序退出时主动销毁实例,可能需要手动实现。
2. 双重检查锁(Double-Checked Locking)
class Singleton {
public:
static Singleton* instance() {
Singleton* tmp = instance_.load(std::memory_order_acquire);
if (!tmp) {
std::lock_guard<std::mutex> lock(mutex_);
tmp = instance_.load(std::memory_order_relaxed);
if (!tmp) {
tmp = new Singleton();
instance_.store(tmp, std::memory_order_release);
}
}
return tmp;
}
// 禁止拷贝与赋值
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() = default;
~Singleton() = default;
static std::atomic<Singleton*> instance_;
static std::mutex mutex_;
};
std::atomic<Singleton*> Singleton::instance_{nullptr};
std::mutex Singleton::mutex_;
- 优点:延迟初始化(第一次访问时才创建),适合需要控制实例创建时机的场景。
- 缺点:代码较为复杂,容易出现细节错误。需要保证内存顺序和正确的同步。
3. 静态局部对象 + 互斥锁(手动控制)
class Singleton {
public:
static Singleton& instance() {
std::call_once(flag_, [](){
instance_ = new Singleton();
});
return *instance_;
}
// 必须保证删除器线程安全
static void destroy() {
std::call_once(destroy_flag_, [](){
delete instance_;
instance_ = nullptr;
});
}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() = default;
~Singleton() = default;
static Singleton* instance_;
static std::once_flag flag_;
static std::once_flag destroy_flag_;
};
Singleton* Singleton::instance_ = nullptr;
std::once_flag Singleton::flag_;
std::once_flag Singleton::destroy_flag_;
- 优点:使用
std::call_once保证一次性初始化,且语义清晰。可以在需要时手动销毁实例。 - 缺点:需要手动调用
destroy(),否则会在程序退出时由系统析构。
4. 基于 std::shared_ptr 的单例(懒加载)
class Singleton {
public:
static std::shared_ptr <Singleton> instance() {
std::call_once(init_flag_, [](){
ptr_ = std::shared_ptr <Singleton>(new Singleton);
});
return ptr_;
}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() = default;
~Singleton() = default;
static std::shared_ptr <Singleton> ptr_;
static std::once_flag init_flag_;
};
std::shared_ptr <Singleton> Singleton::ptr_ = nullptr;
std::once_flag Singleton::init_flag_;
- 优点:返回
std::shared_ptr,方便与其他代码共享生命周期。自动析构。 - 缺点:共享计数导致额外开销。若单例本身不需要被复制,使用裸指针更合适。
5. 线程安全与性能权衡
- 一次性初始化:如果实例不需要懒加载,直接使用 Meyers 单例即可,既安全又高效。
- 延迟创建:若实例初始化代价高且不确定是否会被使用,建议使用双重检查锁或
std::call_once。 - 跨线程访问:所有方法均保证多线程安全,关键点是使用
std::atomic、std::mutex或std::call_once。 - 销毁顺序:在多模块依赖单例时,注意销毁顺序。使用
std::call_once的destroy或std::shared_ptr可以帮助管理生命周期。
6. 小结
- 推荐:对大多数项目,使用 C++11 之后的局部静态变量(Meyers 单例)是最简单、最安全、性能最优的方案。
- 特殊需求:若需要延迟加载、显式销毁或自定义内存管理,可考虑
std::call_once或双重检查锁实现。
通过以上方法,你可以在 C++ 项目中灵活、安全地实现单例模式,满足不同场景下的需求。