在多线程环境下,单例模式(Singleton Pattern)常被用来保证全局资源的唯一性。然而,若实现不当,可能导致竞争条件、重复实例化或性能瓶颈。下面给出几种现代C++实现线程安全单例的方法,并比较它们的优缺点。
1. 局部静态变量(Meyer’s Singleton)
class Logger {
public:
static Logger& instance() {
static Logger instance; // C++11 之后线程安全的初始化
return instance;
}
void log(const std::string& msg) { /* ... */ }
private:
Logger() = default;
Logger(const Logger&) = delete;
Logger& operator=(const Logger&) = delete;
};
-
优点
- 简单易懂,几行代码即可实现。
- C++11 之后编译器保证局部静态对象的线程安全初始化。
- 不需要手动加锁,避免死锁和锁竞争。
-
缺点
- 资源销毁顺序不确定;若在
main()退出时仍有其他线程访问Logger::instance(),可能导致已销毁对象被访问。 - 无法实现延迟销毁(即需要在程序结束后手动销毁资源)。
- 资源销毁顺序不确定;若在
2. 双重检查锁(Double-Check Locking)
class Config {
public:
static Config* instance() {
if (!ptr_) {
std::lock_guard<std::mutex> lock(mtx_);
if (!ptr_) {
ptr_ = new Config();
}
}
return ptr_;
}
private:
Config() = default;
static std::mutex mtx_;
static Config* ptr_;
};
std::mutex Config::mtx_;
Config* Config::ptr_ = nullptr;
-
优点
- 只在首次实例化时加锁,后续访问几乎无锁。
- 适用于需要显式销毁实例或需要控制实例生命周期的场景。
-
缺点
- 代码复杂,易出错。
- 在某些编译器/体系结构上,若未使用
volatile或std::atomic,可能出现指令重排导致的可见性问题。 - 需要手动销毁
ptr_,否则可能导致内存泄漏。
3. std::call_once 与 std::once_flag
class Resource {
public:
static Resource& instance() {
std::call_once(flag_, [](){
instance_ = new Resource();
});
return *instance_;
}
private:
Resource() = default;
static Resource* instance_;
static std::once_flag flag_;
};
Resource* Resource::instance_ = nullptr;
std::once_flag Resource::flag_;
-
优点
- 线程安全,语义明确。
call_once的实现通常使用轻量级同步(如自旋锁或原子操作)。- 与双重检查锁相比,代码更简洁、可维护性更高。
-
缺点
- 需要手动销毁实例。
- 仍然是“单例”对象的全局生命周期管理,无法在程序运行时中途销毁。
4. 现代 C++ 之 std::unique_ptr + std::mutex
class Cache {
public:
static Cache& get() {
std::lock_guard<std::mutex> lock(mtx_);
if (!instance_) {
instance_ = std::make_unique <Cache>();
}
return *instance_;
}
void set(const std::string& key, int value) { /* ... */ }
private:
Cache() = default;
static std::mutex mtx_;
static std::unique_ptr <Cache> instance_;
};
std::mutex Cache::mtx_;
std::unique_ptr <Cache> Cache::instance_;
-
优点
- 自动管理内存,避免泄漏。
- 通过
unique_ptr让实例在程序退出时安全销毁。
-
缺点
- 仍需加锁,虽然锁粒度小,但多次获取实例会产生锁竞争。
- 对性能敏感的场景需要考虑更轻量化的实现。
5. 对比与最佳实践
| 方法 | 初始化是否线程安全 | 锁开销 | 资源销毁 | 可读性 | 推荐场景 |
|---|---|---|---|---|---|
| 局部静态变量 | ✅ | 0 | 由编译器控制 | ✅ | 典型单例 |
| 双重检查锁 | ⚠️ | 低 | 手动 | ⚠️ | 需要手动销毁 |
call_once |
✅ | 低 | 手动 | ✅ | 需要一次性初始化 |
unique_ptr+mutex |
✅ | 中 | 自动 | ✅ | 需要可控销毁 |
- 对于绝大多数业务场景,局部静态变量(Meyer’s Singleton)是最推荐的实现方式。
- 若需显式销毁或延迟初始化,建议使用
std::call_once结合std::unique_ptr。 - 双重检查锁仅在非常特殊的低级优化场景使用,且需要确保编译器/平台对内存可见性的严格保证。
6. 小结
C++11 之后,线程安全的单例实现变得简单且可靠。通过正确的同步原语(std::call_once、std::mutex 或局部静态变量),可以在保证多线程安全的前提下,保持代码简洁。记住:不要因为想避免锁而采用不安全的双重检查锁,因为可见性和指令重排问题会导致难以追踪的错误。保持实现简单、可读、易维护,才是高质量 C++ 编程的关键。