单例模式要求一个类在程序运行期间只能存在一个实例,同时提供全局的访问入口,在配置管理、日志模块等场景中应用十分广泛。多线程环境下,多个线程同时尝试获取单例实例时,普通的非线程安全实现可能会导致实例被多次创建,破坏单例的约束。

普通单例的线程安全问题
最常见的懒汉式单例实现如下,这种写法在单线程场景下可以正常工作,但在多线程环境下存在风险:
#include <iostream>
class Singleton {
private:
static Singleton* instance;
// 私有构造函数,禁止外部直接创建实例
Singleton() {
std::cout << "Singleton instance created" << std::endl;
}
public:
static Singleton* getInstance() {
if (instance == nullptr) {
instance = new Singleton();
}
return instance;
}
};
// 静态成员初始化
Singleton* Singleton::instance = nullptr;
int main() {
Singleton* s1 = Singleton::getInstance();
Singleton* s2 = Singleton::getInstance();
std::cout << (s1 == s2) << std::endl; // 单线程下输出1
return 0;
}
当两个线程同时判断instance == nullptr成立时,都会执行new Singleton()操作,最终会创建出两个不同的实例,违背单例的设计初衷。
双重检查锁定的实现思路
双重检查锁定的核心是在加锁前后分别检查实例是否已经创建,第一次检查是为了避免实例已经存在时仍然加锁带来的性能开销,第二次检查是为了防止多个线程同时通过第一次检查后重复创建实例。实现时需要注意内存序的问题,避免指令重排导致的未定义行为。
基础双重锁实现(C++11之前)
C++11之前的标准没有内存序相关的规范,不同编译器对指令重排的处理不同,下面的实现在部分场景下可能存在问题:
#include <iostream>
#include <pthread.h>
class Singleton {
private:
static Singleton* instance;
static pthread_mutex_t mutex;
Singleton() {
std::cout << "Singleton instance created" << std::endl;
}
public:
static Singleton* getInstance() {
// 第一次检查,实例已存在则直接返回,避免加锁开销
if (instance == nullptr) {
pthread_mutex_lock(&mutex);
// 第二次检查,防止多个线程同时通过第一次检查后重复创建
if (instance == nullptr) {
instance = new Singleton();
}
pthread_mutex_unlock(&mutex);
}
return instance;
}
};
Singleton* Singleton::instance = nullptr;
pthread_mutex_t Singleton::mutex = PTHREAD_MUTEX_INITIALIZER;
int main() {
Singleton* s1 = Singleton::getInstance();
Singleton* s2 = Singleton::getInstance();
std::cout << (s1 == s2) << std::endl;
return 0;
}
上述代码中instance = new Singleton()可以拆分为三步:分配内存、调用构造函数、将指针指向分配的内存。编译器可能会将第三步重排到第二步之前,此时另一个线程第一次检查instance不为空,会直接返回一个未完成构造的实例,导致程序出错。
C++11及之后的正确实现
C++11引入了内存序相关的特性,使用std::atomic和std::mutex可以写出线程安全且符合标准的双重锁单例:
#include <iostream>
#include <mutex>
#include <atomic>
class Singleton {
private:
static std::atomic<Singleton*> instance;
static std::mutex mtx;
Singleton() {
std::cout << "Singleton instance created" << std::endl;
}
public:
static Singleton* getInstance() {
// 使用原子操作加载实例指针,避免指令重排问题
Singleton* tmp = instance.load(std::memory_order_acquire);
if (tmp == nullptr) {
std::lock_guard<std::mutex> lock(mtx);
tmp = instance.load(std::memory_order_relaxed);
if (tmp == nullptr) {
tmp = new Singleton();
// 使用释放语义存储指针,保证构造完成后再被其他线程看到
instance.store(tmp, std::memory_order_release);
}
}
return tmp;
}
};
std::atomic<Singleton*> Singleton::instance(nullptr);
std::mutex Singleton::mtx;
int main() {
Singleton* s1 = Singleton::getInstance();
Singleton* s2 = Singleton::getInstance();
std::cout << (s1 == s2) << std::endl;
return 0;
}
这里使用std::atomic的memory_order_acquire和memory_order_release来保证内存序:存储实例时使用释放语义,加载实例时使用获取语义,确保其他线程能正确看到实例的完整构造过程,避免指令重排带来的问题。
更简洁的C++11单例实现
实际上C++11之后的标准保证了函数内静态局部变量的初始化是线程安全的,因此可以用更简洁的方式实现线程安全单例,不需要手动处理锁和原子操作:
#include <iostream>
class Singleton {
private:
Singleton() {
std::cout << "Singleton instance created" << std::endl;
}
public:
static Singleton& getInstance() {
// 静态局部变量,C++11保证初始化线程安全
static Singleton instance;
return instance;
}
// 禁止拷贝和赋值
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
};
int main() {
Singleton& s1 = Singleton::getInstance();
Singleton& s2 = Singleton::getInstance();
std::cout << (&s1 == &s2) << std::endl;
return 0;
}
这种实现方式代码更简洁,也不容易出错,是C++11及之后版本实现单例模式的推荐方案。如果需要在不支持C++11的老版本环境中实现线程安全单例,再考虑使用双重检查锁定的方式。
实现注意事项
- 单例类的构造函数、拷贝构造函数、赋值运算符都需要设为私有或删除,避免外部创建或拷贝实例。
- 双重检查锁定实现中必须使用原子操作或者内存屏障来避免指令重排问题,否则可能出现未定义行为。
- 锁的粒度要尽可能小,第一次检查的目的就是减少加锁的频率,提升多线程场景下的性能。
- 如果单例实例需要销毁,要注意销毁顺序的问题,避免其他单例依赖当前单例时已经被销毁。