在多线程环境下,单例模式需要确保只有一个实例被创建,并且在并发访问时不会出现竞态条件。下面介绍几种常见的线程安全实现方式,并给出完整的代码示例。
1. 经典局部静态变量(C++11以后)
class Singleton {
public:
static Singleton& instance() {
static Singleton instance; // 线程安全的局部静态变量
return instance;
}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() = default;
};
- 优点:实现简单,编译器保证局部静态变量在第一次访问时线程安全。
- 缺点:如果需要延迟销毁,或者在程序结束前手动销毁实例,可能需要额外处理。
2. 带锁的懒汉式
#include <mutex>
class Singleton {
public:
static Singleton& instance() {
std::call_once(initFlag, [](){ instancePtr = new Singleton(); });
return *instancePtr;
}
// 需要时手动销毁
static void destroy() {
delete instancePtr;
instancePtr = nullptr;
}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() = default;
static Singleton* instancePtr;
static std::once_flag initFlag;
};
Singleton* Singleton::instancePtr = nullptr;
std::once_flag Singleton::initFlag;
- 优点:
std::call_once保证只调用一次初始化函数,性能较好。 - 缺点:需要手动管理销毁,避免内存泄漏。
3. 双检锁(Double-Checked Locking)+ 原子操作
#include <atomic>
#include <mutex>
class Singleton {
public:
static Singleton* instance() {
Singleton* tmp = instancePtr.load(std::memory_order_acquire);
if (!tmp) {
std::lock_guard<std::mutex> lock(mtx);
tmp = instancePtr.load(std::memory_order_relaxed);
if (!tmp) {
tmp = new Singleton();
instancePtr.store(tmp, std::memory_order_release);
}
}
return tmp;
}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() = default;
static std::atomic<Singleton*> instancePtr;
static std::mutex mtx;
};
std::atomic<Singleton*> Singleton::instancePtr{nullptr};
std::mutex Singleton::mtx;
- 优点:在多数线程已创建实例后,访问时不需要加锁,提升性能。
- 缺点:实现稍复杂,且在旧编译器上可能出现指令重排序导致的问题。
4. Meyers 单例与 C++17 std::optional
#include <optional>
class Singleton {
public:
static Singleton& instance() {
static std::optional <Singleton> instance{Singleton()};
return *instance;
}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() = default;
};
- 优点:使用
std::optional可以在需要时手动销毁实例。 - 缺点:代码略显冗长,适用于特殊需求。
何时使用哪种实现?
| 场景 | 推荐实现 |
|---|---|
| 简单单例,程序生命周期内一次性创建 | 局部静态变量(Meyers) |
| 需要显式销毁或多次创建/销毁 | 带锁的懒汉式 + std::call_once |
| 访问频繁,想减少锁开销 | 双检锁 + 原子 |
| C++17 环境,想在需要时销毁 | std::optional |
常见错误与调试技巧
-
双检锁实现未使用
std::memory_order- 可能导致指令重排序,使得未完全构造的对象被返回。
- 解决:使用
std::memory_order_acquire/release。
-
忘记
delete单例- 可能造成内存泄漏。
- 解决:在
atexit注册销毁函数,或使用std::shared_ptr结合std::weak_ptr。
-
多线程调试时出现死锁
- 确认锁只在第一次初始化时使用,后续访问不需要加锁。
- 使用
std::call_once可避免此类错误。
结语
线程安全的单例模式在 C++ 中并不是一门难题,关键在于理解编译器如何保证局部静态变量的初始化安全,以及何时需要手动管理生命周期。根据项目需求、性能考虑和代码可维护性选择合适的实现方式,即可在多线程环境中稳定使用单例。