在C++中,单例模式(Singleton)用于保证一个类只有一个实例,并且在整个程序生命周期内都可被全局访问。实现单例模式时最关键的挑战是确保在多线程环境下,单例实例的创建是线程安全的。下面将从理论和实践两个角度,探讨几种常见的线程安全实现方式,并给出完整代码示例。
1. 理论基础
-
双重检查锁定(Double-Checked Locking, DCL)
- 通过先检查实例是否已存在,若不存在再加锁,再检查一次,最后创建实例。
- 在C++11之前,DCL由于内存模型不完善,存在“指令重排”导致的线程安全问题。
- 在C++11之后,只要使用
std::atomic或std::once_flag,DCL就可以安全实现。
-
局部静态变量
- C++11之后,局部静态变量的初始化是线程安全的。
- 代码最简洁,且不需要显式锁。
- 适合单例无参构造或构造不需要外部资源。
-
Meyer’s Singleton
- 通过使用局部静态对象实现单例。
- 同样依赖C++11的线程安全初始化机制。
-
std::call_once+std::once_flag- 通过一次性调用机制,确保某个函数只执行一次,且线程安全。
- 适用于需要更细粒度控制的场景。
2. 代码实现
2.1 局部静态变量实现(Meyer’s Singleton)
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(mtx_);
std::cout << "[LOG] " << msg << std::endl;
}
private:
Logger() = default;
Logger(const Logger&) = delete;
Logger& operator=(const Logger&) = delete;
std::mutex mtx_;
};
2.2 std::call_once 实现
class ConfigManager {
public:
static ConfigManager& instance() {
std::call_once(initFlag_, []() {
instance_ = new ConfigManager();
});
return *instance_;
}
std::string get(const std::string& key) const {
std::lock_guard<std::mutex> lock(mtx_);
auto it = config_.find(key);
return it != config_.end() ? it->second : "";
}
void set(const std::string& key, const std::string& value) {
std::lock_guard<std::mutex> lock(mtx_);
config_[key] = value;
}
private:
ConfigManager() { /* 读取配置文件 */ }
~ConfigManager() = default;
static ConfigManager* instance_;
static std::once_flag initFlag_;
std::mutex mtx_;
std::unordered_map<std::string, std::string> config_;
};
ConfigManager* ConfigManager::instance_ = nullptr;
std::once_flag ConfigManager::initFlag_;
2.3 双重检查锁定(DCL)实现(C++11)
class Database {
public:
static Database* getInstance() {
Database* tmp = instance_.load(std::memory_order_acquire);
if (!tmp) {
std::lock_guard<std::mutex> lock(mtx_);
tmp = instance_.load(std::memory_order_relaxed);
if (!tmp) {
tmp = new Database();
instance_.store(tmp, std::memory_order_release);
}
}
return tmp;
}
// ...
private:
Database() { /* 连接数据库 */ }
~Database() = default;
Database(const Database&) = delete;
Database& operator=(const Database&) = delete;
static std::atomic<Database*> instance_;
static std::mutex mtx_;
};
std::atomic<Database*> Database::instance_{nullptr};
std::mutex Database::mtx_;
3. 选型建议
| 场景 | 推荐实现 |
|---|---|
| 只需单例、无外部依赖 | 局部静态变量(Meyer’s) |
| 需要按需初始化、外部资源 | std::call_once + std::once_flag |
| 兼容旧编译器(C++03) | 手写双重检查锁定 + pthread/std::mutex(需注意指令重排) |
| 线程安全且性能要求极高 | 对象池 + 预创建实例,或使用自旋锁优化 |
4. 常见坑与解决方案
-
构造函数抛异常
- 如果构造函数可能抛异常,使用
std::call_once可以避免泄漏,因为异常后实例化不会写入指针。
- 如果构造函数可能抛异常,使用
-
多线程销毁
- 采用局部静态实现时,实例在程序结束时自动销毁,线程安全。
- 如果手动管理内存,需要在程序退出前保证没有线程仍在使用实例。
-
测试线程安全
- 编写单元测试,启动多线程并多次访问
instance(),检查是否只产生一次实例。
- 编写单元测试,启动多线程并多次访问
-
性能考虑
- 局部静态变量实现每次访问都需要检查是否已初始化,虽然成本很小,但如果单例访问频繁且对性能极致要求,可考虑使用“懒汉+缓存”方案。
5. 结语
C++11 引入的线程安全局部静态初始化以及 std::call_once/std::once_flag,让实现线程安全单例变得异常简单和可靠。开发者只需要根据具体业务需求选择合适的实现方式,即可在多线程环境中安全、高效地使用单例。祝编码愉快!