单例模式(Singleton)保证一个类只有一个实例,并提供全局访问点。在多线程环境下,若实现不当,可能会产生多实例或资源竞争。下面从多个角度展示在C++中实现线程安全单例的常见手段,并讨论其优缺点。
1. Meyers单例(局部静态变量)
class Logger {
public:
static Logger& instance() {
static Logger logger; // C++11 起线程安全
return logger;
}
void log(const std::string& msg) {
std::lock_guard<std::mutex> lock(mutex_);
std::cout << msg << std::endl;
}
private:
Logger() = default;
~Logger() = default;
Logger(const Logger&) = delete;
Logger& operator=(const Logger&) = delete;
std::mutex mutex_;
};
- 优点:实现最简洁;C++11 之后编译器保证局部静态变量初始化线程安全;延迟实例化,第一次访问时才创建。
- 缺点:无法在实例销毁前自定义初始化顺序(例如需要先初始化其他全局对象)。
2. std::call_once 与 std::once_flag
class Config {
public:
static Config& getInstance() {
std::call_once(initFlag_, [](){
instance_ = new Config(loadFromFile());
});
return *instance_;
}
private:
explicit Config(const std::map<std::string, std::string>& cfg)
: data_(cfg) {}
static Config* instance_;
static std::once_flag initFlag_;
std::map<std::string, std::string> data_;
};
Config* Config::instance_ = nullptr;
std::once_flag Config::initFlag_;
- 优点:对复杂的初始化流程(如读取配置文件、网络请求)可做一次性初始化,且线程安全。
- 缺点:需要手动管理析构(上例中未显式销毁),如果不想手动释放,可以使用
std::unique_ptr或std::shared_ptr。
3. 双重检查锁(Double-Check Locking)
class Database {
public:
static Database* getInstance() {
if (!instance_) {
std::lock_guard<std::mutex> lock(mutex_);
if (!instance_) {
instance_ = new Database();
}
}
return instance_;
}
private:
Database() = default;
static Database* instance_;
static std::mutex mutex_;
};
Database* Database::instance_ = nullptr;
std::mutex Database::mutex_;
- 优点:只有第一次访问需要加锁,性能相对更优。
- 缺点:在C++中实现难度大,尤其是
instance_必须是std::atomic<Database*>,否则可能出现指令重排导致读到未构造对象。
4. 智能指针 + std::shared_ptr
class Service {
public:
static std::shared_ptr <Service> getInstance() {
std::call_once(initFlag_, [](){
instance_ = std::make_shared <Service>(initialize());
});
return instance_;
}
private:
explicit Service(const Config& cfg) : config_(cfg) {}
static std::shared_ptr <Service> instance_;
static std::once_flag initFlag_;
Config config_;
};
std::shared_ptr <Service> Service::instance_ = nullptr;
std::once_flag Service::initFlag_;
- 优点:自动销毁,线程安全且避免裸指针。
- 缺点:每次访问需要获取共享指针,略微增加开销。
5. 对象池式实现(更通用)
template <typename T>
class Singleton {
public:
template <typename... Args>
static T& instance(Args&&... args) {
std::call_once(flag_, [&]{
ptr_.reset(new T(std::forward <Args>(args)...));
});
return *ptr_;
}
private:
static std::unique_ptr <T> ptr_;
static std::once_flag flag_;
};
template <typename T>
std::unique_ptr <T> Singleton<T>::ptr_ = nullptr;
template <typename T>
std::once_flag Singleton <T>::flag_;
使用方式:
class Engine { /* ... */ };
Engine& eng = Singleton <Engine>::instance(/* constructor args */);
- 优点:复用代码,支持不同类型单例。
- 缺点:模板实现复杂度略高。
6. 何时选择哪种实现?
| 场景 | 推荐实现 | 说明 |
|---|---|---|
| 仅需延迟实例化,且初始化不依赖外部资源 | Meyers 单例 | 简洁,C++11 线程安全 |
| 初始化过程复杂(I/O、网络) | std::call_once |
能保证一次性初始化 |
| 需要自定义销毁顺序 | 静态对象 + atexit 或 std::unique_ptr |
可在程序退出前释放 |
| 想避免全局对象初始化顺序问题 | std::shared_ptr + call_once |
自动管理生命周期 |
| 需要多种类型单例 | 模板 `Singleton | |
| ` | 统一接口 |
7. 结语
在现代 C++(C++11 及以后)里,单例模式实现已大大简化。最常见、最安全的做法是使用局部静态变量(Meyers单例),因为编译器已内置线程安全保证。若有更复杂需求,可以结合 std::call_once 或模板包装来满足。记住:单例不等于“万能”,在高并发或模块化设计中往往更推荐使用依赖注入或服务定位器,以保持代码的可测试性与可维护性。