在多线程环境下,保证单例对象的线程安全性是一个常见的挑战。C++11 引入了线程安全的静态局部变量初始化,极大地方便了单例实现。下面将从三个角度展开讨论:Meyers 单例、双检锁(Double-Checked Locking)以及基于 std::call_once 的实现。
1. Meyers 单例(线程安全的静态局部变量)
class Logger {
public:
static Logger& getInstance() {
static Logger instance; // C++11 规定线程安全的初始化
return instance;
}
void log(const std::string& msg) {
std::lock_guard<std::mutex> lock(mtx_);
std::cout << msg << std::endl;
}
private:
Logger() = default;
Logger(const Logger&) = delete;
Logger& operator=(const Logger&) = delete;
std::mutex mtx_;
};
优点
- 代码简洁,完全利用语言特性。
- 无需显式锁或同步机制,避免死锁与性能问题。
缺点
- 仅在 C++11 之后可用。
- 不能在实例销毁前做自定义操作(除非手动实现
std::unique_ptr或std::shared_ptr)。
2. 双检锁(Double-Checked Locking)
在 C++11 之前,双检锁是实现线程安全单例的常用手段。由于编译器优化和 CPU 指令重排,传统实现可能出现数据竞争。使用 std::atomic 可以确保可见性。
#include <atomic>
#include <mutex>
class Config {
public:
static Config* getInstance() {
Config* tmp = instance_.load(std::memory_order_acquire);
if (!tmp) {
std::lock_guard<std::mutex> lock(mtx_);
tmp = instance_.load(std::memory_order_relaxed);
if (!tmp) {
tmp = new Config();
instance_.store(tmp, std::memory_order_release);
}
}
return tmp;
}
private:
Config() = default;
static std::atomic<Config*> instance_;
static std::mutex mtx_;
};
std::atomic<Config*> Config::instance_{nullptr};
std::mutex Config::mtx_;
优点
- 兼容旧标准(C++03)。
- 对象初始化延迟,首次调用时才创建。
缺点
- 代码更繁琐,易出错。
- 需要手动管理内存,易导致内存泄漏。
3. 基于 std::call_once 的实现
C++11 标准库提供 std::call_once,可以让你只调用一次某个函数,天然线程安全。
#include <mutex>
class Service {
public:
static Service& getInstance() {
std::call_once(initFlag_, []() {
instance_ = new Service();
});
return *instance_;
}
private:
Service() = default;
static Service* instance_;
static std::once_flag initFlag_;
};
Service* Service::instance_ = nullptr;
std::once_flag Service::initFlag_;
优点
- 代码简洁,易维护。
- 对象销毁时仍然可以自定义析构顺序(如
std::atexit)。
缺点
- 仍需手动处理内存(除非使用
std::unique_ptr)。 - 与
Meyers单例相比,略有性能开销(调用一次std::call_once的成本)。
选型建议
| 实现方式 | 适用场景 | 主要优势 | 主要劣势 |
|---|---|---|---|
| Meyers 单例 | C++11+ | 简洁、性能优越 | 无法在销毁前自定义 |
| 双检锁 | C++03 | 兼容旧标准 | 复杂、易出错 |
std::call_once |
C++11+ | 灵活、线程安全 | 需手动内存管理 |
在现代 C++ 项目中,推荐使用 Meyers 单例 或 std::call_once 结合 std::unique_ptr,既保证线程安全,又避免手动内存管理的风险。
结语
单例模式本质上是“唯一实例”的实现,真正需要关注的往往不是单例本身,而是 线程安全性、初始化时机 与 资源释放。掌握 C++11 及以后的特性后,单例的实现可以做到既安全又简洁。希望本文能帮助你在项目中做出合适的选择。