在多线程环境下,单例模式需要保证以下两点:
- 只创建一次实例;
- 多线程并发访问时不产生竞态条件。
下面介绍几种常用实现方式,并讨论它们的优缺点。
1. 经典懒汉式 + 双重检查锁(Double‑Check Locking)
class Singleton {
public:
static Singleton& instance() {
if (instance_ == nullptr) { // 第一次检查
std::lock_guard<std::mutex> lock(mutex_);
if (instance_ == nullptr) { // 第二次检查
instance_ = new Singleton();
}
}
return *instance_;
}
// 禁止拷贝构造和赋值
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() = default;
~Singleton() = default;
static Singleton* instance_;
static std::mutex mutex_;
};
Singleton* Singleton::instance_ = nullptr;
std::mutex Singleton::mutex_;
优点
- 延迟初始化,真正需要时才创建实例。
- 线程安全,使用
std::mutex防止竞态。
缺点
- 代码较为繁琐。
- 在 C++11 以前的编译器中,
instance_的写操作可能不可见,导致缺陷;C++11 之后已修正。
2. Meyer’s 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 以后,局部静态变量的初始化是线程安全的,编译器会在第一次进入 instance() 时保证单例被正确构造。
优点
- 代码最简洁。
- 自动销毁,程序退出时析构函数被调用。
缺点
- 仍是懒汉式,如果实例创建时出现异常,后续调用会再次尝试。
- 对析构顺序要求严格,若单例持有全局资源,可能导致析构顺序不确定。
3. 静态局部对象 + std::call_once
class Singleton {
public:
static Singleton& instance() {
std::call_once(flag_, [](){ instance_ = new Singleton(); });
return *instance_;
}
~Singleton() {
delete instance_;
instance_ = nullptr;
}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() = default;
static Singleton* instance_;
static std::once_flag flag_;
};
Singleton* Singleton::instance_ = nullptr;
std::once_flag Singleton::flag_;
优点
- 与
Meyers兼容,确保单例只被初始化一次。 std::once_flag只需要一次检查,性能优于双重检查锁。
缺点
- 需要手动管理析构,容易忘记。
4. 基于 std::shared_ptr 的单例(可自毁)
class Singleton {
public:
static std::shared_ptr <Singleton> instance() {
static std::shared_ptr <Singleton> instance(new Singleton(),
[](Singleton* p){ delete p; });
return instance;
}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() = default;
};
使用 std::shared_ptr 可以让单例在不再使用时自动销毁,适用于需要在特定时机释放资源的场景。
优点
- 自动销毁,避免全局静态析构顺序问题。
缺点
- 需要额外的引用计数开销。
5. 线程安全的初始化顺序控制
如果单例中需要依赖其它全局对象,建议使用 “单例先构造,其他后析构” 的模式:
class Singleton {
public:
static Singleton& instance() {
static Singleton instance; // 首先构造
return instance;
}
// ...
};
void foo() {
// 使用单例
Singleton::instance().doSomething();
}
因为 C++11 之后,编译器保证局部静态变量在第一次使用时初始化,并且在程序结束时按逆序析构,确保单例始终存在于其生命周期内。
小结
- 最简洁:Meyer’s Singleton(局部静态对象)。
- 性能最优:
std::call_once。 - 可自毁:
std::shared_ptr。
在实际项目中,推荐使用 Meyer's Singleton 或 std::call_once 的组合,既保证线程安全,又代码简洁。若对资源释放有特殊需求,可考虑 std::shared_ptr 或手动析构方式。