单例模式(Singleton)是一种常见的设计模式,保证一个类只有一个实例,并提供全局访问点。在多线程环境下,若不做正确处理,可能会导致多个实例被创建,从而破坏单例的本意。下面给出几种在C++中实现线程安全单例的方式,并对每种方案的优缺点进行简要说明。
1. Meyers 单例(局部静态变量)
class Singleton {
public:
static Singleton& instance() {
static Singleton instance; // C++11起,局部静态变量初始化是线程安全的
return instance;
}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() {}
};
-
优点
- 简单、代码量少。
- 由于 C++11 标准规定,局部静态变量的初始化是线程安全的,编译器会自动插入必要的同步机制。
- 延迟加载:实例在第一次调用时才创建。
-
缺点
- 需要 C++11 或更高版本。
- 对于需要提前初始化或在销毁时执行特定操作的场景不够灵活。
2. 双重检查锁(Double-Checked Locking)
#include <mutex>
class Singleton {
public:
static Singleton* instance() {
if (ptr == nullptr) { // 第一次检查
std::lock_guard<std::mutex> lock(mtx);
if (ptr == nullptr) { // 第二次检查
ptr = new Singleton();
}
}
return ptr;
}
private:
Singleton() {}
static Singleton* ptr;
static std::mutex mtx;
};
Singleton* Singleton::ptr = nullptr;
std::mutex Singleton::mtx;
-
优点
- 兼容 C++03,适用于旧项目。
- 只在第一次需要时才加锁,后续访问成本低。
-
缺点
- 需要使用
volatile或std::atomic来保证内存可见性,否则存在重排序导致的未初始化对象泄露风险。 - 代码较为繁琐,易出错。
- 需要使用
3. 静态指针与 std::call_once
#include <mutex>
class Singleton {
public:
static Singleton& instance() {
std::call_once(initFlag, [](){
instancePtr = new Singleton();
});
return *instancePtr;
}
private:
Singleton() {}
static Singleton* instancePtr;
static std::once_flag initFlag;
};
Singleton* Singleton::instancePtr = nullptr;
std::once_flag Singleton::initFlag;
-
优点
- 明确的单次初始化语义,易于理解。
- 对于复杂的初始化过程,可以在 lambda 中执行任何操作。
- 兼容 C++11 及以上。
-
缺点
- 需要手动管理实例的销毁(可以借助
std::unique_ptr或atexit)。 - 与
Meyers单例相比略显冗余。
- 需要手动管理实例的销毁(可以借助
4. 使用 std::unique_ptr + 静态局部
#include <memory>
class Singleton {
public:
static Singleton& instance() {
static std::unique_ptr <Singleton> ptr{new Singleton};
return *ptr;
}
private:
Singleton() {}
};
-
优点
- 自动管理内存,避免泄漏。
- 适用于需要在程序退出时执行析构时做额外清理的情况。
-
缺点
- 与
Meyers方案类似,仍依赖 C++11。
- 与
5. 编译器扩展:__declspec(thread)(仅限 MSVC)
class Singleton {
public:
static Singleton& instance() {
thread_local Singleton instance; // 每个线程有自己的实例
return instance;
}
};
- 说明
这不是传统意义上的全局单例,而是线程局部单例(Thread-Local Singleton)。在某些场景下,例如需要每个线程拥有独立实例但又想统一管理资源时,可以使用。
6. 何时选用哪种方案?
| 场景 | 推荐方案 |
|---|---|
| 需要兼容 C++03 | 双重检查锁或 std::call_once(自定义实现) |
| 只需简单单例 | Meyers 单例(C++11) |
| 需要在初始化前后做复杂操作 | std::call_once + lambda |
| 想避免手动 delete | std::unique_ptr + 静态局部 |
| 需要线程局部单例 | thread_local 关键字 |
7. 小结
线程安全的单例实现并非一成不变,选择合适的方案取决于项目的 C++ 标准、性能需求以及初始化/销毁的复杂度。最常见且最简洁的方式是 Meyers 单例,它利用 C++11 对局部静态变量初始化的线程安全保证,几乎无需额外代码。然而,对于需要兼容旧标准或更细粒度控制的情况,std::call_once 或双重检查锁仍然是可靠的备选方案。请根据项目实际情况进行选型,并结合单元测试验证多线程环境下的正确性。