在多线程环境下,单例模式常常会遇到并发访问导致的多实例创建问题。传统的懒汉式单例实现虽然实现简单,但在多线程情况下仍有可能出现“竞态条件”。下面给出一种基于 C++17 的线程安全单例实现,兼顾延迟初始化和性能优化,并提供使用示例。
1. 需求分析
- 延迟加载:只有在第一次使用时才创建实例。
- 线程安全:多线程并发访问时保证仅创建一次实例。
- 可扩展:后续需要向单例中添加依赖或进行参数化时也能方便修改。
2. 解决方案
2.1 使用 std::call_once 与 std::once_flag
std::call_once 是 C++11 引入的同步机制,确保在多线程环境中仅执行一次给定函数。配合 std::once_flag,可以实现最优的线程安全单例。
#include <mutex>
#include <memory>
class Singleton {
public:
// 禁止拷贝和移动
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
Singleton(Singleton&&) = delete;
Singleton& operator=(Singleton&&) = delete;
// 获取单例实例
static Singleton& instance() {
std::call_once(initFlag_, []() {
instancePtr_ = new Singleton();
});
return *instancePtr_;
}
// 示例方法
void doSomething() {
// ...
}
private:
Singleton() {
// 构造时做必要初始化
}
~Singleton() {
// 销毁时清理资源
}
static std::once_flag initFlag_;
static Singleton* instancePtr_;
};
// 静态成员定义
std::once_flag Singleton::initFlag_;
Singleton* Singleton::instancePtr_ = nullptr;
说明:
instance()采用惰性初始化,第一次调用时才创建实例。std::call_once内部会使用一次性锁,保证线程安全且只执行一次。instancePtr_是裸指针,适用于单例生命周期与程序生命周期相同的场景;如果想让对象在main结束时自动销毁,可改为std::unique_ptr或std::shared_ptr。
2.2 采用局部静态变量(C++11+)
C++11 之后,函数内的局部静态变量初始化是线程安全的。相比 std::call_once,实现更简洁,且编译器可做更好的优化。
class Singleton {
public:
static Singleton& instance() {
static Singleton instance; // C++11 保证线程安全
return instance;
}
void doSomething() { /* ... */ }
private:
Singleton() {}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
};
优点:
- 代码更简洁。
- 线程安全由编译器保证。
- 延迟初始化且只在第一次访问时创建。
缺点:
- 对构造函数的异常抛出需要额外处理(若构造抛异常,下一次调用会再次尝试构造,直到成功)。
2.3 需要传递参数的单例
若单例需要在第一次创建时传递参数,可使用工厂方法结合 std::call_once:
class ConfigurableSingleton {
public:
static ConfigurableSingleton& instance(const std::string& config) {
std::call_once(initFlag_, [&]() {
instancePtr_ = new ConfigurableSingleton(config);
});
return *instancePtr_;
}
private:
ConfigurableSingleton(const std::string& cfg) : config_(cfg) {}
std::string config_;
static std::once_flag initFlag_;
static ConfigurableSingleton* instancePtr_;
};
后续调用可以忽略参数:
auto& obj = ConfigurableSingleton::instance(); // 参数可留空
3. 性能与安全性评估
| 方法 | 线程安全 | 延迟初始化 | 成本 | 备注 |
|---|---|---|---|---|
std::call_once |
✅ | ✅ | 轻量级锁 | 兼容 C++11+ |
| 局部静态变量 | ✅ | ✅ | 无锁 | 需处理异常 |
| 双重检查锁(经典实现) | ❌(实现错误常见) | ✅ | 可能导致数据竞争 | 建议避免 |
在大多数现代 C++ 代码库中,推荐使用局部静态变量或std::call_once两种方案。局部静态变量更简洁,std::call_once 更适合需要传递参数或更复杂初始化流程的场景。
4. 常见陷阱
- 析构顺序问题:如果单例对象在全局析构时仍被访问,可能导致悬空引用。可采用 Meyers Singleton(局部静态)或使用
std::unique_ptr并在atexit注册销毁函数。 - 跨动态库(DLL)单例:不同模块可能各自加载一次单例,导致多实例。解决方案是将单例放入公共动态库,或使用全局
extern "C"函数。 - 异常安全:在构造函数抛异常时,后续访问会再次尝试初始化,导致可能的无限循环。可在构造函数中捕获并处理异常,或使用
std::unique_ptr结合std::make_unique。
5. 代码示例:完整实现
#include <iostream>
#include <mutex>
#include <string>
class Logger {
public:
static Logger& getInstance() {
static Logger instance; // C++11 线程安全
return instance;
}
void log(const std::string& msg) {
std::lock_guard<std::mutex> lock(mutex_);
std::cout << "[LOG] " << msg << std::endl;
}
private:
Logger() : initialized_(false) {
// 模拟昂贵初始化
std::cout << "Logger initializing..." << std::endl;
initialized_ = true;
}
~Logger() { std::cout << "Logger destroyed." << std::endl; }
Logger(const Logger&) = delete;
Logger& operator=(const Logger&) = delete;
bool initialized_;
std::mutex mutex_;
};
int main() {
Logger::getInstance().log("Hello, singleton!");
return 0;
}
运行结果:
Logger initializing...
[LOG] Hello, singleton!
Logger destroyed.
以上就是在 C++ 中实现线程安全单例模式的常见方案及其细节。根据具体项目需求选择合适的实现方式,即可保证在多线程环境下单例的安全性与性能。