在多线程环境下,单例模式的实现需要保证:
- 只创建一次实例;
- 在实例创建期间不会出现竞争条件;
- 同时保持高性能,不给每一次访问都加锁。
以下是几种常见的实现方式,并分别讨论它们的优缺点。
1. Meyer’s Singleton(局部静态变量)
class Singleton {
public:
static Singleton& instance() {
static Singleton obj; // C++11 之后编译器保证线程安全
return obj;
}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() = default;
};
说明
- 线程安全:C++11 标准规定局部静态变量在第一次使用时的初始化是线程安全的。
- 懒加载:实例只有在
instance()第一次被调用时才创建。 - 实现简单:无须显式锁。
缺点
- 无法在编译时控制实例化时间:如果想在程序启动前就实例化,需要显式调用
instance()。 - 不可销毁:对象会在程序退出时按自然顺序析构,若有依赖顺序的析构需求需要额外处理。
2. 双重检查锁(Double-Check Locking)
class Singleton {
public:
static Singleton* instance() {
if (ptr_ == nullptr) { // 第一重检查
std::lock_guard<std::mutex> lock(mutex_);
if (ptr_ == nullptr) { // 第二重检查
ptr_ = new Singleton();
}
}
return ptr_;
}
~Singleton() { delete ptr_; }
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() = default;
static Singleton* ptr_;
static std::mutex mutex_;
};
Singleton* Singleton::ptr_ = nullptr;
std::mutex Singleton::mutex_;
说明
- 懒加载 + 手动控制:在需要时才创建,且可以决定何时销毁。
- 多线程安全:使用互斥锁保证唯一性。
缺点
- 性能开销:第一次实例化时需要加锁,且在每次访问时都会进行两次空指针检查。
- 实现细节:需要正确使用
volatile或std::atomic,否则可能出现指令重排导致的“半初始化”对象。
3. std::call_once + std::once_flag
class Singleton {
public:
static Singleton& instance() {
std::call_once(initFlag_, [](){ instancePtr_ = new Singleton(); });
return *instancePtr_;
}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() = default;
static Singleton* instancePtr_;
static std::once_flag initFlag_;
};
Singleton* Singleton::instancePtr_ = nullptr;
std::once_flag Singleton::initFlag_;
说明
- 一次性初始化:
std::call_once保证闭包只执行一次,无论有多少线程并发访问。 - 线程安全且性能更佳:相比双重检查锁,
std::call_once的实现通常更高效。
缺点
- 同样是手动销毁:需要在适当的时机手动删除实例,否则会造成内存泄漏。
4. 基于 C++17 的 inline 变量
class Singleton {
public:
static Singleton& instance() {
static Singleton obj;
return obj;
}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() = default;
};
inline Singleton& getSingleton() {
return Singleton::instance();
}
说明
inline变量在 C++17 后允许在头文件中定义,避免多定义错误。- 与 Meyer’s 方案等价,只是更明确表达实现细节。
5. 线程安全的懒加载 + 对象销毁顺序
如果单例依赖其他全局对象,需要控制销毁顺序,可以使用 std::unique_ptr 并配合 std::atexit:
class Singleton {
public:
static Singleton& instance() {
static std::unique_ptr <Singleton> ptr;
if (!ptr) {
ptr.reset(new Singleton());
std::atexit([](){ ptr.reset(); }); // 程序结束时销毁
}
return *ptr;
}
// ...
};
总结
- 最推荐:使用 Meyer’s Singleton(局部静态变量),因其实现简单且符合 C++11 标准的线程安全保证。
- 特殊需求:若需要手动销毁或在编译期确定实例化时间,
std::call_once或双重检查锁是更灵活的选择。 - 注意:在任何实现中都要删除拷贝构造和赋值操作,避免被错误复制。
通过选择合适的实现方式,可以在多线程环境中安全、高效地使用单例模式。