在多线程环境下,单例模式的实现往往会遇到并发安全问题。下面以 C++17 为例,介绍几种常用且线程安全的实现方式,并说明各自的优缺点。
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() = default;
};
-
优点
- 代码简洁,编译器负责初始化顺序。
- C++11 引入的局部静态变量初始化是线程安全的,避免了显式锁。
-
缺点
- 延迟初始化:如果程序未访问
instance(),对象永不创建。 - 对于类构造失败时的异常处理,只有在第一次访问时才会抛出。
- 延迟初始化:如果程序未访问
2. 双重检查锁(Double-Checked Locking)
class Singleton {
public:
static Singleton* getInstance() {
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;
}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() = default;
static std::atomic<Singleton*> instance_;
static std::mutex mutex_;
};
std::atomic<Singleton*> Singleton::instance_{nullptr};
std::mutex Singleton::mutex_;
-
优点
- 仅在第一次创建时使用锁,后续访问无锁开销。
- 适用于需要手动销毁或在全局析构顺序中控制的情况。
-
缺点
- 代码复杂,容易出现细微错误(如内存序问题)。
- 在 C++11 之前,双重检查锁并不保证线程安全。
3. 经典的 std::call_once
class Singleton {
public:
static Singleton& instance() {
std::call_once(flag_, []() { ptr_ = new Singleton(); });
return *ptr_;
}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() = default;
static Singleton* ptr_;
static std::once_flag flag_;
};
Singleton* Singleton::ptr_ = nullptr;
std::once_flag Singleton::flag_;
-
优点
- 代码可读性好,内部使用
std::once_flag保障一次性执行。 - 适合需要显式销毁或在某些平台上控制初始化顺序的情况。
- 代码可读性好,内部使用
-
缺点
- 仍然需要手动管理内存,若不销毁会造成内存泄漏。
- 对比
Meyers单例,稍有性能损耗(一次锁判断)。
4. std::shared_ptr 结合 std::weak_ptr
class Singleton {
public:
static std::shared_ptr <Singleton> instance() {
std::lock_guard<std::mutex> lock(mutex_);
if (!ptr_) {
ptr_ = std::shared_ptr <Singleton>(new Singleton(), [](Singleton*){});
}
return ptr_;
}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() = default;
static std::weak_ptr <Singleton> ptr_;
static std::mutex mutex_;
};
std::weak_ptr <Singleton> Singleton::ptr_;
std::mutex Singleton::mutex_;
-
优点
- 通过
std::weak_ptr可以在需要时检测实例是否已被销毁。 - 自动内存管理,避免手动
delete。
- 通过
-
缺点
- 仍然需要显式锁。
weak_ptr的使用稍显冗余,除非需要监控实例生命周期。
小结
- 最推荐:Meyers 单例(局部静态变量)——最简洁、线程安全、几乎无性能损耗。
- 需要显式销毁:
std::call_once或std::shared_ptr/std::weak_ptr方案。 - 高性能需求:双重检查锁,但必须确保正确使用原子和内存序。
在实际项目中,除非你有特殊需求(如需要在多线程程序退出前手动销毁单例),否则建议使用第一种方法,以保持代码的简洁与安全。