在多线程环境下,单例模式的实现需要确保只有一个实例被创建,并且在并发访问时不会出现竞争条件。下面以 C++17 为例,演示几种常见的线程安全实现方式,并说明各自的优缺点。
1. C++11 的 std::call_once
最直接、最推荐的方式是使用 std::call_once 和 std::once_flag。它们是标准库提供的原子操作,天然线程安全,且在第一次调用时才会初始化。
#include <iostream>
#include <mutex>
class Singleton {
public:
static Singleton& instance() {
std::call_once(initFlag_, []() { instancePtr_ = new Singleton(); });
return *instancePtr_;
}
// 禁止拷贝和赋值
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
void sayHello() const { std::cout << "Hello from Singleton\n"; }
private:
Singleton() { std::cout << "Singleton constructed\n"; }
~Singleton() { std::cout << "Singleton destroyed\n"; }
static Singleton* instancePtr_;
static std::once_flag initFlag_;
};
Singleton* Singleton::instancePtr_ = nullptr;
std::once_flag Singleton::initFlag_;
优点
- 简洁:代码量少,易于维护。
- 高效:只有第一次调用会触发一次性初始化,后续调用几乎不消耗资源。
- 标准:属于 C++ 标准库,保证在所有标准实现上的行为一致。
缺点
- 需要手动管理单例的销毁,如果不使用
std::unique_ptr或std::shared_ptr,程序退出时会留下悬挂的静态对象,可能导致析构顺序问题。
2. 局部静态变量(Meyers Singleton)
C++11 之后,局部静态变量的初始化已被保证为线程安全。只需在函数内部声明静态对象即可。
class Singleton {
public:
static Singleton& instance() {
static Singleton instance; // 线程安全初始化
return instance;
}
// ...
};
优点
- 最简洁:几乎不需要任何额外代码。
- 自动销毁:静态对象在程序结束时按正确顺序析构。
缺点
- 延迟初始化:如果
instance()从未被调用,单例永不构造。 - 难以控制:无法显式控制实例的生命周期,例如提前销毁或重建。
3. 双重检查锁(Double-Checked Locking)
传统的双重检查锁方案在 C++11 之前不安全,但使用 std::atomic 后可以工作。示例代码:
#include <atomic>
#include <mutex>
class Singleton {
public:
static Singleton* instance() {
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;
}
// ...
private:
Singleton() = default;
static std::atomic<Singleton*> instance_;
static std::mutex mutex_;
};
std::atomic<Singleton*> Singleton::instance_{nullptr};
std::mutex Singleton::mutex_;
优点
- 延迟初始化:首次访问时才构造。
- 性能:在已初始化后,无需加锁即可获取实例。
缺点
- 实现繁琐:需要仔细处理内存序和锁,错误可能导致难以调试的并发 bug。
- 不推荐:
std::call_once更加直观且已被标准化。
4. 线程局部单例(Thread-Local Singleton)
有时你需要为每个线程维护一个独立的单例实例。可以使用 thread_local 关键字。
class ThreadSingleton {
public:
static ThreadSingleton& instance() {
thread_local ThreadSingleton instance;
return instance;
}
// ...
};
适用场景
- 需要线程级别的隔离,例如线程安全的日志记录器。
- 不想共享状态导致竞争的场景。
小结
- 推荐:使用
std::call_once+std::once_flag或者局部静态变量(Meyers)实现单例。 - 不推荐:双重检查锁需要细致的原子操作,容易出错。
- 特殊需求:如果每个线程需要独立实例,考虑
thread_local。
下面给出一个完整示例,演示在多线程环境下安全获取单例,并记录线程 ID:
#include <iostream>
#include <thread>
#include <mutex>
#include <vector>
#include <chrono>
class Logger {
public:
static Logger& instance() {
static Logger instance; // 线程安全初始化
return instance;
}
void log(const std::string& msg) {
std::lock_guard<std::mutex> lock(mutex_);
std::cout << "[Thread " << std::this_thread::get_id() << "] " << msg << '\n';
}
private:
Logger() = default;
~Logger() = default;
std::mutex mutex_;
};
void worker(int id) {
for (int i = 0; i < 5; ++i) {
Logger::instance().log("Worker " + std::to_string(id) + " iteration " + std::to_string(i));
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
}
int main() {
const int numThreads = 4;
std::vector<std::thread> threads;
for (int i = 0; i < numThreads; ++i) {
threads.emplace_back(worker, i);
}
for (auto& t : threads) t.join();
return 0;
}
运行结果显示,所有线程共享同一个 Logger 实例,输出顺序不确定,但线程 ID 一致,证明单例在多线程环境下的正确性。