在多线程环境下实现一个单例对象时,最常见的难点是保证对象只被创建一次且在所有线程之间安全可见。下面以C++17为例,演示几种常用且线程安全的实现方式,并对其优缺点进行简要讨论。
1. 本地静态变量(Meyers单例)
class Singleton {
public:
static Singleton& instance() {
static Singleton inst; // C++11 起线程安全
return inst;
}
// 其他公共接口
void do_something() { /* ... */ }
private:
Singleton() = default; // 私有构造
~Singleton() = default; // 私有析构
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
};
原理
在 C++11 之后,编译器保证对局部静态对象的初始化是线程安全的。首次访问 instance() 时,inst 以原子方式完成构造,随后所有线程都能安全访问同一实例。
优点
- 代码简洁,几乎不需要额外的同步机制。
- 延迟初始化,直到真正需要实例时才构造。
缺点
- 无法控制实例的销毁时机(在程序退出时自动销毁)。
- 对于需要按需销毁或重置的单例场景不够灵活。
2. std::call_once 与 std::unique_ptr
#include <memory>
#include <mutex>
class Singleton {
public:
static Singleton& instance() {
std::call_once(initFlag, []{
instancePtr.reset(new Singleton());
});
return *instancePtr;
}
void do_something() { /* ... */ }
private:
Singleton() = default;
~Singleton() = default;
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
static std::unique_ptr <Singleton> instancePtr;
static std::once_flag initFlag;
};
std::unique_ptr <Singleton> Singleton::instancePtr;
std::once_flag Singleton::initFlag;
原理
std::call_once 只会让第一次调用时执行给定的 lambda,其余线程会等待直到初始化完成。std::unique_ptr 管理实例生命周期。
优点
- 可在需要时显式销毁单例(如
instancePtr.reset()),满足某些应用需求。 - 与
std::call_once的语义更清晰,易于理解。
缺点
- 代码略显冗长,需手动维护静态成员。
3. 原子指针 + 双重检查锁(DCL)
#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;
}
void do_something() { /* ... */ }
private:
Singleton() = default;
~Singleton() = default;
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
static std::atomic<Singleton*> instancePtr;
static std::mutex mtx;
};
std::atomic<Singleton*> Singleton::instancePtr{nullptr};
std::mutex Singleton::mtx;
原理
第一次检测到 instancePtr 为 nullptr 后,线程会尝试获取互斥锁并再次检查,确保只有一个线程执行实例化。std::memory_order_acquire/release 保证可见性。
优点
- 适用于需要在不同平台上手动控制同步细节的老旧代码。
- 可在 C++11 之前使用(只需自行实现原子与锁)。
缺点
- 代码更易出错,必须正确使用内存序。
- 过度同步会导致性能瓶颈,尤其在实例已创建后每次访问仍需检查
instancePtr。
4. 对比与选择
| 方法 | 线程安全性 | 是否延迟初始化 | 销毁控制 | 代码复杂度 |
|---|---|---|---|---|
| 本地静态变量 | ✅ | ✅ | 自动 | 低 |
call_once/unique_ptr |
✅ | ✅ | 手动 | 中 |
| 双重检查锁 | ✅ | ✅ | 手动 | 高 |
| 传统单例(无同步) | ❌ | ❌ | 难 | 低 |
- 最推荐:若项目已使用 C++11 或更高版本,首选 本地静态变量。其实现最简洁,且线程安全保证由标准提供。
- 需要销毁控制:使用
call_once+unique_ptr或std::shared_ptr(若需要共享所有权)来显式管理单例生命周期。 - 老项目或特殊需求:若项目对同步细节有特殊需求(如需要自定义内存序),可考虑 双重检查锁。
5. 小结
在多线程环境下实现单例模式时,关键是保证“只创建一次”和“所有线程可见”。自 C++11 起,标准库提供了足够成熟的工具(std::call_once、局部静态变量)来实现这一点,开发者不必再手动写复杂的锁代码。只有在极少数情况下(如需要自定义销毁时机、支持共享所有权或兼容旧编译器)才需要使用更复杂的方案。选择合适的实现方式,既能保证线程安全,又能保持代码简洁与易维护。