在现代 C++(C++11 及以后)中,创建线程安全的单例最简洁、最安全的办法是利用函数内的静态局部变量。由于 C++11 标准保证对函数内局部静态变量的初始化是线程安全的,所有线程在第一次调用函数时都会获得同一个实例,而不必担心竞争条件。下面给出完整示例,并解释关键点和常见误区。
// Singleton.h
#pragma once
#include <mutex>
#include <memory>
class Singleton
{
public:
// 获取全局唯一实例
static Singleton& instance()
{
// C++11 之下,局部静态变量初始化是线程安全的
static Singleton instance;
return instance;
}
// 禁止拷贝构造和赋值
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
// 示例方法
void doSomething()
{
std::lock_guard<std::mutex> lock(mutex_);
// 业务逻辑
++counter_;
}
int getCounter() const { return counter_; }
private:
// 构造函数私有,避免外部实例化
Singleton() : counter_(0) {}
mutable std::mutex mutex_;
int counter_;
};
// main.cpp
#include "Singleton.h"
#include <thread>
#include <vector>
#include <iostream>
void worker()
{
for (int i = 0; i < 1000; ++i)
{
Singleton::instance().doSomething();
}
}
int main()
{
const int threadCount = 8;
std::vector<std::thread> workers;
for (int i = 0; i < threadCount; ++i)
workers.emplace_back(worker);
for (auto& t : workers)
t.join();
std::cout << "Final counter value: " << Singleton::instance().getCounter() << std::endl;
return 0;
}
关键点解析
-
静态局部变量初始化
C++11 起,编译器会在首次进入instance()时以线程安全的方式初始化instance。如果你使用的是更早的标准,需使用std::call_once或自旋锁来手动保证线程安全。 -
私有构造
通过私有构造函数确保外部不能直接创建对象,只能通过instance()获取。 -
禁止拷贝/赋值
delete关键字防止单例被复制或赋值,保持唯一性。 -
使用
std::mutex保护实例内部状态
虽然实例的创建线程安全,但单例内部数据的访问仍需同步。在示例中,doSomething()用std::lock_guard保证计数器递增的原子性。 -
惰性初始化
只有在真正需要实例时才会被创建,避免不必要的开销,且能够处理类静态成员与全局变量初始化顺序问题。
常见误区
-
双重检查锁定(Double-Checked Locking)
早期的实现往往使用if (!instance_) { std::lock_guard<std::mutex> lock(mtx); if (!instance_) instance_ = new Singleton(); }。这种写法在某些平台下会出现可见性问题。自 C++11 起,直接使用静态局部变量更安全简洁。 -
使用
` 可能导致在多线程环境下出现“自引用”或“悬挂指针”。静态局部对象的生命周期与程序相同,避免了所有权管理的额外复杂性。std::shared_ptr
直接返回 `std::shared_ptr -
*把
instance()设为 `static Singleton** 如果你返回裸指针,需要手动保证单例在程序结束前不会被销毁。使用引用(Singleton&`)可以避免这种错误。 -
懒加载与性能
对于极端高性能需求,静态局部变量的初始化开销通常可以忽略。若真的需要进一步优化,可考虑在程序启动时显式构造单例。
小结
- 在 C++11 及以后,函数内静态局部变量是实现线程安全单例最推荐的方式。
- 通过私有构造、禁止拷贝、内部同步保护,可以保证单例的唯一性与线程安全。
- 只需一行
static Singleton instance;,其余细节遵循标准规则即可。
这样,你就拥有了一个既安全又高效的单例实现,适用于日志、配置、数据库连接池等场景。