在现代 C++(尤其是 C++11 及之后的标准)里,线程安全的单例模式实现已经变得相当简单。传统的单例实现往往依赖双重检查锁定(Double-Checked Locking,DCL)或在初始化阶段手动加锁,而这些做法既容易出错,又会带来不必要的性能开销。下面我们将系统地阐述 C++11 里最优雅、最安全的单例实现方式,并展示几种常见的变体与适用场景。
1. 经典单例实现回顾
在单线程环境中,最常见的单例实现是:
class Singleton {
public:
static Singleton& getInstance() {
static Singleton instance; // 静态局部变量
return instance;
}
private:
Singleton() {}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
};
这个实现依赖编译器对静态局部变量的初始化顺序以及“静态局部变量初始化的线程安全保证”。在 C++11 之前,static 局部变量在多线程调用 getInstance 时可能会出现竞态条件;但自 C++11 起,标准保证它是线程安全的。
1.1 双重检查锁定(DCL)
为了避免每次 getInstance 调用都必须获取锁,很多实现采用双重检查锁定:
class Singleton {
public:
static Singleton* getInstance() {
if (instance_ == nullptr) { // 第一次检查
std::lock_guard<std::mutex> lock(mutex_);
if (instance_ == nullptr) { // 第二次检查
instance_ = new Singleton();
}
}
return instance_;
}
private:
Singleton() {}
static Singleton* instance_;
static std::mutex mutex_;
};
DCL 在 C++11 之前并不安全,因为编译器可能对对象的构造过程进行重排;C++11 通过 std::atomic 以及 memory_order 解决了一部分问题,但实现仍然较为复杂且易错。鉴于 C++11 之后提供了更为简单且安全的方式,推荐使用静态局部变量实现。
2. C++11 的线程安全静态局部变量
C++11 规定,静态局部变量在首次执行时的初始化是原子且线程安全的。实现的核心是:
class Singleton {
public:
static Singleton& instance() {
static Singleton instance; // 编译器确保线程安全
return instance;
}
// ...
private:
Singleton() {}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
};
这段代码的优势:
- 延迟初始化:对象只有在第一次调用
instance()时才被创建,避免了程序启动时就创建不必要的资源。 - 线程安全:标准保证在多线程环境下同一时间只有一个线程能够完成初始化,其他线程会等待。
- 简单易读:不需要显式锁或原子指针,代码极其简洁。
3. 细节与常见误区
3.1 对象生命周期
使用静态局部变量时,对象的销毁顺序不确定,可能在 main() 返回之后,或在 std::atexit 里被销毁。若单例内部持有 std::thread、std::mutex 或其他系统资源,建议在类中显式提供 destroy() 方法,以便程序显式释放资源。
class Singleton {
public:
static Singleton& instance() {
static Singleton instance;
return instance;
}
static void destroy() {
// 手动销毁
// 这里可以通过 std::unique_ptr 或者析构函数完成
}
private:
Singleton() {}
~Singleton() {}
// ...
};
3.2 多继承与虚继承
如果单例类使用多继承,尤其是虚继承,可能导致多重构造与销毁。此时建议使用模板或组合方式,将单例作为基类,并在派生类中提供单例访问。
template <typename T>
class Singleton {
public:
static T& instance() {
static T instance;
return instance;
}
};
然后:
class MyService : public Singleton <MyService> {
// ...
};
3.3 线程池与资源竞争
在实际项目中,单例往往需要访问全局资源(数据库连接池、日志系统等)。确保这些资源本身是线程安全的非常重要。例如,日志系统应使用锁或原子操作;数据库连接池可以采用 std::mutex 或 std::shared_mutex。
4. 高级变体:懒汉式与饿汉式
- 懒汉式(Lazy):如上所示,使用静态局部变量按需初始化。适合资源开销大,且不一定在程序启动时就需要的情况。
- 饿汉式(Eager):在程序启动时就创建实例。实现方式:
class Singleton {
public:
static Singleton& instance() {
return *instance_;
}
private:
Singleton() {}
static Singleton* instance_;
};
Singleton* Singleton::instance_ = new Singleton();
饿汉式的优势是更易于析构顺序管理(因为对象在全局初始化时就已创建),缺点是无论是否使用,资源都会被初始化,且在多线程环境下可能导致初始化竞态。
5. 结合 C++17 的 std::call_once
如果你想保持显式控制初始化流程,可以使用 std::call_once 与 std::once_flag:
class Singleton {
public:
static Singleton& instance() {
std::call_once(flag_, [](){ instance_.reset(new Singleton()); });
return *instance_;
}
private:
Singleton() {}
static std::unique_ptr <Singleton> instance_;
static std::once_flag flag_;
};
std::unique_ptr <Singleton> Singleton::instance_;
std::once_flag Singleton::flag_;
虽然语法更冗长,但你可以在 call_once 的 lambda 内完成更复杂的初始化逻辑,例如读取配置文件、连接网络等。
6. 总结
- C++11 之后,最推荐的实现方式是使用 线程安全的静态局部变量,代码简洁且标准保证安全。
- 对于需要 显式控制初始化顺序 或 多继承 的情况,可以考虑 模板单例 或
std::call_once。 - 记住 资源的正确释放 与 析构顺序,尤其在多线程程序中,错误的析构可能导致崩溃或内存泄漏。
通过掌握上述实现技巧,你可以在 C++ 项目中稳健地使用单例模式,并充分利用现代语言特性带来的便利与安全保障。