在 C++ 中实现单例(Singleton)模式时,最常见的目标之一是确保在多线程环境下也能安全地创建和访问唯一实例。自 C++11 起,语言标准为此提供了多种工具与语义保证,让我们能够以更简洁、更安全的方式实现单例。下面我们分别介绍几种常用的实现方案,并对其优缺点做一番总结。
1. Meyers 单例(局部静态变量)
class Singleton {
public:
static Singleton& getInstance() {
static Singleton instance; // C++11 之后此处为线程安全
return instance;
}
// 需要复制构造与赋值操作符时可加上禁止
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() = default;
~Singleton() = default;
};
为什么线程安全?
C++11 对局部静态变量的初始化做了线程安全的保证:若多个线程同时进入 getInstance(),编译器会自动生成同步代码,确保 instance 只被初始化一次,且所有线程都会看到完整初始化后的对象。
优点
- 简洁,几行代码即可完成
- 无需手动加锁,避免死锁与性能瓶颈
缺点
- 只能在 C++11 及以上编译器使用
- 若构造函数抛异常,后续调用
getInstance()时会重新尝试初始化,导致错误处理复杂
2. std::call_once 与 std::once_flag
class Singleton {
public:
static Singleton& getInstance() {
std::call_once(initFlag, [](){ instance = new Singleton(); });
return *instance;
}
static void destroy() {
std::call_once(destroyFlag, [](){ delete instance; instance = nullptr; });
}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() = default;
~Singleton() = default;
static Singleton* instance;
static std::once_flag initFlag;
static std::once_flag destroyFlag;
};
Singleton* Singleton::instance = nullptr;
std::once_flag Singleton::initFlag;
std::once_flag Singleton::destroyFlag;
原理
std::call_once 只会执行一次指定的函数,std::once_flag 用来标记。即使多个线程并发调用 getInstance(),编译器也会保证 initFlag 只会触发一次实例化。
优点
- 与任何 C++ 标准兼容(C++11 及以上)
- 允许在单例被显式销毁后再次创建(如需要清理资源)
缺点
- 代码稍显冗长
- 需要手动管理单例生命周期(
destroy()),否则可能导致泄漏
3. 双重检查锁(Double-Check Locking)
虽然在 C++11 之后不再推荐使用,但在一些旧代码库中仍会看到:
class Singleton {
public:
static Singleton* getInstance() {
if (!instance) {
std::lock_guard<std::mutex> lock(mtx);
if (!instance) {
instance = new Singleton();
}
}
return instance;
}
// ... 其它成员
private:
Singleton() = default;
static Singleton* instance;
static std::mutex mtx;
};
Singleton* Singleton::instance = nullptr;
std::mutex Singleton::mtx;
注意
在 C++11 之前,内存模型不保证对 instance 的写入对其他线程可见,容易出现“脏读”。C++11 的 std::atomic 或者 std::memory_order 可以解决,但整体仍比局部静态变量或 call_once 复杂。
4. 采用 std::shared_ptr 管理生命周期
如果单例需要在多处共享并自动销毁,可以用 std::shared_ptr:
class Singleton {
public:
static std::shared_ptr <Singleton> getInstance() {
static std::shared_ptr <Singleton> instance(new Singleton(),
[](Singleton* p){ delete p; }); // 自定义删除器
return instance;
}
private:
Singleton() = default;
};
优点
- 自动回收资源,避免手动
destroy() - 兼容多线程,局部静态变量保证线程安全
小结
| 实现方式 | 代码量 | 线程安全保证 | 生命周期控制 | 适用场景 |
|---|---|---|---|---|
| Meyers 单例 | 极少 | 编译器自动 | 静态终止 | 现代 C++ 代码 |
call_once |
中等 | 编译器+库 | 可手动 | 需要显式销毁 |
| 双重检查锁 | 多 | 需要 atomic |
手动 | 老代码兼容 |
shared_ptr |
中等 | 编译器 | 自动 | 需要多处共享且可回收 |
在大多数现代 C++ 项目中,Meyers 单例 已经足够安全且简洁,推荐首选。如果项目需要在运行时显式销毁单例,或者需要兼容更旧的编译器,则使用 std::call_once 或 shared_ptr 的变体。了解并灵活运用这些工具,可以让你在多线程环境下轻松实现安全、可靠的单例模式。