在多线程环境下,单例模式的实现需要保证以下几点:
- 懒初始化:只有在第一次使用时才创建实例。
- 线程安全:多线程同时访问时不会产生竞态条件。
- 防止重复实例:即使在极端竞争条件下也只能产生一个实例。
下面给出几种常见实现方式,并对比其优缺点。
1. C++11 以内存序与局部静态变量
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. std::call_once 与 std::once_flag
#include <mutex>
class Singleton {
public:
static Singleton& instance() {
std::call_once(initFlag_, [](){ instance_ = new Singleton(); });
return *instance_;
}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() = default;
static Singleton* instance_;
static std::once_flag initFlag_;
};
Singleton* Singleton::instance_ = nullptr;
std::once_flag Singleton::initFlag_;
优点
- 适用于任何 C++11 及以上。
std::call_once只保证一次调用,即使多个线程同时进入也不会重复初始化。
缺点
- 手动管理内存,若需要显式销毁需自行实现。
3. 双重检查锁(DCL)+ std::atomic
#include <atomic>
#include <mutex>
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;
static std::atomic<Singleton*> instance_;
static std::mutex mutex_;
};
std::atomic<Singleton*> Singleton::instance_{nullptr};
std::mutex Singleton::mutex_;
优点
- 仅在第一次初始化时产生锁开销,后续调用不受影响。
- 适用于对性能有极端要求的场景。
缺点
- 实现细节复杂,容易出现错误。
- 需要理解内存序与原子操作的细微差别。
4. 静态局部变量与自定义析构顺序
如果想在程序结束时确保单例被正确销毁,可将其包装成局部静态并使用 std::atexit:
class Singleton {
public:
static Singleton& instance() {
static Singleton instance;
std::atexit([](){ /* 可选的清理工作 */ });
return instance;
}
// ...
};
5. 常见陷阱与最佳实践
-
多线程竞争导致重复实例
仅使用new进行懒加载而不加锁,易产生多个实例。 -
静态销毁顺序
在main结束前访问已销毁的单例会导致未定义行为。
解决方案:使用局部静态或std::call_once并保证析构顺序。 -
资源泄漏
手动new需要手动delete,最好使用智能指针(std::unique_ptr)来管理。 -
性能瓶颈
对于不需要延迟初始化的场景,直接在编译时构造可能更高效。
6. 推荐方案
- 如果使用 C++11 及以上:首选 局部静态变量(第 1 方案)。
- 如果对内存使用更细粒度控制:可结合
std::call_once(第 2 方案)。 - 对极端性能要求:可考虑 DCL + atomic(第 3 方案),但需严格测试。
结语
线程安全的单例是并发编程中常见但易出错的设计模式。了解并正确使用 C++11 之后的线程安全特性(如局部静态、std::call_once、原子操作)能大幅简化实现,并避免潜在的竞态与资源泄漏问题。掌握好这些工具后,你可以在任何多线程项目中稳妥地使用单例模式。