在多线程环境下,单例模式的实现往往会引发线程安全与性能的双重挑战。下面给出一种既安全又高效的懒加载单例实现方式,并结合 C++17 的特性进行说明。
1. 传统实现的不足
最常见的懒加载单例实现是:
class Singleton {
public:
static Singleton& getInstance() {
if (!instance) {
std::lock_guard<std::mutex> lock(mtx);
if (!instance)
instance = new Singleton();
}
return *instance;
}
private:
Singleton() = default;
~Singleton() = default;
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
static Singleton* instance;
static std::mutex mtx;
};
这种“双检查锁定”方式在 C++11 以前存在指令重排导致的安全隐患,且每次访问都要锁定,性能不佳。
2. C++11 的 call_once 与 once_flag
C++11 引入了 std::call_once 与 std::once_flag,专门用来保证一次性初始化。其实现原理使用了内部锁,且只会在第一次调用时执行初始化代码,后续调用会跳过锁。示例:
class Singleton {
public:
static Singleton& getInstance() {
std::call_once(initFlag, []() {
instance = new Singleton();
});
return *instance;
}
private:
Singleton() = default;
~Singleton() = default;
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
static Singleton* instance;
static std::once_flag initFlag;
};
优点:
- 线程安全:
call_once内部实现已解决指令重排问题。 - 性能优异:初始化完成后不再加锁。
3. C++17 的 std::shared_ptr 与 std::make_shared
若单例对象需要支持多态或智能指针管理,可以改用 std::shared_ptr:
class Singleton {
public:
static std::shared_ptr <Singleton> getInstance() {
std::call_once(initFlag, []() {
instance = std::make_shared <Singleton>();
});
return instance;
}
private:
Singleton() = default;
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
static std::shared_ptr <Singleton> instance;
static std::once_flag initFlag;
};
使用 shared_ptr 可以让单例在整个程序生命周期结束时自动销毁,避免内存泄漏。
4. 进一步优化:使用局部静态变量
C++11 之后,局部静态变量的初始化已保证线程安全。因此,最简洁且无外部同步开销的实现如下:
class Singleton {
public:
static Singleton& getInstance() {
static Singleton instance; // 第一次调用时线程安全地初始化
return instance;
}
private:
Singleton() = default;
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
};
此实现的优势在于:
- 无需显式同步:编译器保证线程安全。
- 延迟加载:只有真正调用
getInstance()时才会创建实例。 - 无资源泄漏:实例随程序结束自动销毁。
5. 何时选择哪种实现?
| 需求 | 推荐实现 |
|---|---|
| 需要多态、共享计数 | std::shared_ptr + call_once |
| 简单单例,关注性能 | 局部静态变量(C++11+) |
| 旧编译器(<C++11) | 双检查锁定 + 自行实现内存屏障 |
6. 代码完整示例(C++17)
#include <iostream>
#include <memory>
#include <mutex>
class Logger {
public:
static std::shared_ptr <Logger> instance() {
std::call_once(initFlag, [](){
inst = std::make_shared <Logger>();
});
return inst;
}
void log(const std::string& msg) {
std::lock_guard<std::mutex> lock(ioMutex);
std::cout << "[" << threadId() << "] " << msg << std::endl;
}
private:
Logger() = default;
~Logger() = default;
Logger(const Logger&) = delete;
Logger& operator=(const Logger&) = delete;
static std::shared_ptr <Logger> inst;
static std::once_flag initFlag;
std::mutex ioMutex;
static std::thread::id threadId() {
return std::this_thread::get_id();
}
};
std::shared_ptr <Logger> Logger::inst = nullptr;
std::once_flag Logger::initFlag;
int main() {
auto logger = Logger::instance();
logger->log("程序启动");
return 0;
}
此代码演示了如何在多线程程序中安全、延迟地创建并使用单例对象。通过 std::call_once 与 std::once_flag,保证了单例在任何并发环境下只被创建一次,并且不会产生锁竞争。
结语
在 C++ 中实现线程安全的懒加载单例并不需要复杂的锁逻辑,现代标准提供了高效且安全的工具。合理选择合适的实现方式,既能满足性能需求,又能保持代码简洁与可维护性。