在现代 C++ 开发中,单例模式仍然是一个常见的设计模式,用于保证某个类只有一个实例并且在整个程序生命周期内都可被全局访问。由于多线程环境的出现,如何在保持单例特性的同时实现线程安全,成为实现这一模式的关键点。本文将通过多种实现方式,讨论其优缺点,并给出推荐的实现方案。
1. 基础单例实现
class Singleton {
public:
static Singleton& getInstance() {
static Singleton instance; // C++11 之后的局部静态变量是线程安全的
return instance;
}
// 禁止拷贝构造和赋值操作
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() {}
~Singleton() {}
};
上述实现利用 C++11 对局部静态变量的线程安全保证,简单易懂。只要编译器符合标准,即可确保 instance 在首次访问时只被初始化一次,随后所有线程共享同一实例。
2. 双重检查锁(Double-Check Locking)
在 C++11 之前,常见的做法是使用互斥量与双重检查锁,以避免每次访问都需要加锁。
#include <mutex>
class Singleton {
public:
static Singleton* getInstance() {
if (!instance_) {
std::lock_guard<std::mutex> lock(mutex_);
if (!instance_) {
instance_ = new Singleton();
}
}
return instance_;
}
// ...
private:
Singleton() {}
~Singleton() {}
static Singleton* instance_;
static std::mutex mutex_;
};
Singleton* Singleton::instance_ = nullptr;
std::mutex Singleton::mutex_;
缺点:双重检查锁在 C++11 之前的标准里并不保证原子性,可能导致数据竞争。即使在 C++11 之后,如果不使用 std::atomic,也可能出现可见性问题。为此,推荐使用 std::call_once 或者局部静态变量实现。
3. std::call_once 方案
#include <mutex>
class Singleton {
public:
static Singleton& getInstance() {
std::call_once(initFlag_, []() {
instance_ = new Singleton();
});
return *instance_;
}
// ...
private:
Singleton() {}
~Singleton() {}
static Singleton* instance_;
static std::once_flag initFlag_;
};
Singleton* Singleton::instance_ = nullptr;
std::once_flag Singleton::initFlag_;
std::call_once 只会执行一次初始化函数,且是线程安全的。缺点是需要手动管理内存,且销毁时需要手动 delete。可以结合 std::unique_ptr 或 std::shared_ptr 简化内存管理。
4. Meyer’s Singleton 与延迟销毁
最简洁且安全的实现是 Meyer’s Singleton。其优点:
- 线程安全:C++11 之后编译器保证局部静态变量初始化时线程安全。
- 延迟销毁:对象在程序退出时由运行时负责销毁,避免了手动删除的麻烦。
- 零成本:访问时不需要加锁,性能最佳。
class Singleton {
public:
static Singleton& getInstance() {
static Singleton instance; // 静态局部变量
return instance;
}
// ...
private:
Singleton() {}
~Singleton() {}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
};
如果你需要在多线程环境中访问单例并且对性能有严格要求,推荐使用 Meyer’s Singleton。
5. 在 C++17 及之后的 std::shared_mutex
当单例需要读写分离,或者需要在多线程中频繁读、偶尔写时,可以使用 std::shared_mutex 来提高并发性能。
#include <shared_mutex>
class Singleton {
public:
static Singleton& getInstance() {
static Singleton instance;
return instance;
}
void setData(const std::string& val) {
std::unique_lock lock(mutex_);
data_ = val;
}
std::string getData() const {
std::shared_lock lock(mutex_);
return data_;
}
private:
Singleton() = default;
mutable std::shared_mutex mutex_;
std::string data_;
};
6. 常见陷阱与注意事项
- 拷贝构造与赋值:始终删除拷贝构造和赋值操作,以防止误创建副本。
- 多继承:如果单例类继承自其他类,尤其是多继承,需确保基类不包含
static成员导致多实例。 - 析构顺序:Meyer’s Singleton 在程序结束时由 C++ 运行时销毁,顺序是逆序。若单例持有对其他单例的引用,需注意销毁顺序,否则可能出现悬空指针。
- 线程安全保证:如果使用旧编译器或 C++11 之前的标准,建议不要使用局部静态变量实现,需要使用
std::call_once或手动锁定。
7. 结论
在现代 C++(C++11 及之后)环境下,Meyer’s Singleton(局部静态变量实现)是最推荐的单例实现方式。它既简单、易读,又能满足线程安全与延迟销毁的需求。若你在更老的编译环境中工作,std::call_once 也是一个安全且优雅的替代方案。通过正确的设计与实现,单例模式可以在多线程程序中保持其可维护性与性能优势。