单例模式(Singleton Pattern)是设计模式之一,其核心思想是确保一个类在整个程序生命周期内只有一个实例,并且为全局提供访问点。在C++中实现线程安全的单例模式,一般有以下几种常见方式:
- Meyers 单例(C++11 之后的局部静态变量)
- 双重检查锁(Double‑Check Locking)
- std::call_once + std::once_flag
下面分别介绍并给出示例代码。
1. Meyers 单例(局部静态变量)
C++11 起,局部静态变量的初始化是线程安全的。最简洁、最推荐的方式:
class Singleton {
public:
static Singleton& instance() {
static Singleton instance; // C++11 之后线程安全
return instance;
}
// 其他公共接口
void doSomething() { /* ... */ }
private:
Singleton() { /* 构造逻辑 */ }
~Singleton() { /* 析构逻辑 */ }
// 禁止拷贝构造和赋值
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
};
优点
- 简单易懂
- 编译器保证线程安全
- 延迟初始化(第一次调用
instance()时才创建)
缺点
- 在某些老旧编译器(C++11 之前)不可行
- 如果构造函数抛异常,后续调用仍会继续尝试重新初始化(但同样是线程安全的)
2. 双重检查锁(Double‑Check Locking)
适用于旧编译器或需要自定义初始化逻辑时。关键是使用 std::atomic 或 volatile 与互斥量结合。
#include <atomic>
#include <mutex>
class Singleton {
public:
static Singleton* instance() {
Singleton* tmp = instance_.load(std::memory_order_acquire);
if (!tmp) {
std::lock_guard<std::mutex> lock(mutex_);
tmp = instance_.load(std::memory_order_relaxed);
if (!tmp) {
tmp = new Singleton();
instance_.store(tmp, std::memory_order_release);
}
}
return tmp;
}
// ...
private:
Singleton() {}
~Singleton() {}
static std::atomic<Singleton*> instance_;
static std::mutex mutex_;
};
std::atomic<Singleton*> Singleton::instance_{nullptr};
std::mutex Singleton::mutex_;
注意事项
instance_必须是std::atomic,否则并发读写会出现数据竞争。std::memory_order的使用确保正确的可见性。- 仍然要防止析构时多线程访问的问题。
3. std::call_once + std::once_flag
这是 C++11 标准库提供的最安全、最简洁的实现方式:
#include <mutex>
class Singleton {
public:
static Singleton& instance() {
std::call_once(flag_, []() { instance_ = new Singleton(); });
return *instance_;
}
// ...
private:
Singleton() {}
~Singleton() {}
static Singleton* instance_;
static std::once_flag flag_;
};
Singleton* Singleton::instance_ = nullptr;
std::once_flag Singleton::flag_;
优点
call_once确保初始化只执行一次,即使多线程并发访问。- 不需要手动使用互斥锁。
缺点
- 仍然使用裸指针,需要自行管理析构。可以改为
std::unique_ptr。
4. 何时使用哪种方式?
| 方案 | 适用场景 | 主要特点 |
|---|---|---|
| Meyers 单例 | C++11 及以上 | 简洁,编译器保证线程安全 |
| 双重检查锁 | 旧编译器或需要自定义构造 | 需要手动锁,复杂度较高 |
| call_once | C++11 及以上,需对初始化做额外操作 | 线程安全,易于使用 |
在实际项目中,首选 Meyers 单例,除非你需要在单例构造时做一些复杂的同步操作(例如读取配置文件、建立数据库连接),此时 std::call_once 会更合适。
5. 小结
实现线程安全的单例在 C++ 中非常成熟。利用标准库提供的特性,既可以保证代码的可维护性,又能避免手动管理锁导致的错误。推荐在项目中使用 Meyers 单例 或 std::call_once,这两种方式足以满足绝大多数需求,并且代码简洁、易读。