在多线程环境下实现单例模式时,最关键的问题是保证实例的唯一性与线程安全。下面给出几种常见且高效的实现方式,并对每种方法的优缺点进行简要说明。
1. 局部静态变量(C++11 之后)
class Singleton {
public:
static Singleton& instance() {
static Singleton instance; // C++11 之后保证线程安全
return instance;
}
private:
Singleton() = default;
~Singleton() = default;
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
};
优点
- 代码简洁,易于维护。
- 依赖标准库实现,跨平台性好。
- 只在第一次调用时构造,后续访问无额外开销。
缺点
- 对 C++11 之前的编译器不适用。
- 在极少数情况下,局部静态变量的析构顺序可能导致 “static deinitialization order fiasco”,但这在 C++11 之后已被标准解决。
2. 双重检查锁(DCLP)
class Singleton {
public:
static Singleton* instance() {
if (ptr == nullptr) {
std::lock_guard<std::mutex> lock(mtx);
if (ptr == nullptr) {
ptr = new Singleton();
}
}
return ptr;
}
private:
Singleton() = default;
~Singleton() = default;
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
static Singleton* ptr;
static std::mutex mtx;
};
Singleton* Singleton::ptr = nullptr;
std::mutex Singleton::mtx;
优点
- 对早期的 C++ 标准兼容。
- 只在真正需要创建实例时才加锁,性能相对较好。
缺点
- 实现细节繁琐,容易出现错误。
- 需要手动管理实例的生命周期(如在程序结束前手动 delete)。
- 在某些编译器/平台上可能出现 “memory reorder” 的问题,需要使用
std::atomic<Singleton*>或者std::atomic<bool>来防止。
3. 枚举单例(Enum)
class Singleton {
public:
static Singleton& instance() {
static Singleton instance;
return instance;
}
private:
Singleton() = default;
~Singleton() = default;
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
};
enum class SingletonHolder { INSTANCE }; // 只用来占位
此方式与第一种方式类似,但利用枚举避免了可能的构造/析构顺序问题。
4. 静态局部变量 + std::call_once
class Singleton {
public:
static Singleton& instance() {
std::call_once(flag, []() {
ptr = new Singleton();
});
return *ptr;
}
private:
Singleton() = default;
~Singleton() = default;
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
static Singleton* ptr;
static std::once_flag flag;
};
Singleton* Singleton::ptr = nullptr;
std::once_flag Singleton::flag;
优点
- 兼容 C++03,且线程安全。
- 只执行一次初始化,性能优异。
缺点
- 需要手动释放
ptr,否则可能导致内存泄漏。
小结
- 推荐:若使用 C++11 及以后版本,最简洁且安全的方式是 局部静态变量(第一种)。
- 兼容老版本:可使用
std::call_once或 双重检查锁。 - 需要注意内存管理:若使用
new,务必在程序退出前delete或者使用std::unique_ptr自动释放。
通过以上方法,开发者可以根据项目的编译环境、性能需求以及维护成本来选择最合适的单例实现方式。