单例模式是一种常用的软件设计模式,它确保一个类只有一个实例,并提供一个全局访问点来获取这个实例。下面是一个使用C++实现的线程安全的单例模式的例子:
class Singleton {
private:
static std::atomic<Singleton*> instance; // 静态私有实例指针使用原子操作类atomic线程安全
static std::mutex mutex; // 静态互斥锁
// 私有构造函数,防止外部直接创建实例,只能内部调用 相当于只会创建一个
Singleton() {
std::cout << "Singleton instance created." << std::endl;
}
// 禁止复制构造函数和赋值操作符
Singleton(const Singleton& other) = delete;
Singleton& operator=(const Singleton& other) = delete;
public:
// 静态公有方法,提供全局访问点
static Singleton* getInstance() {
// 双重检查锁定模式,减少锁的使用
if (instance == nullptr) {
std::lock_guardstd::mutex lock(mutex);
if (instance == nullptr) { // 再次检查,确保线程安全
instance = new Singleton();
}
}
return instance;
}
// 示例方法
void doSomething() {
std::cout << "Doing something." << std::endl;
}
// 销毁单例实例的方法(通常在程序结束时调用)
static void destroyInstance() {
delete instance;
instance = nullptr;
std::cout << "Singleton instance destroyed." << std::endl;
}
};
// 初始化静态成员变量
Singleton* Singleton::instance = nullptr;
std::mutex Singleton::mutex;
getInstance方法返回的一个对象在单线程是绝对安全的,但是在多线程的情况下,我们就不一定了。加入线程A访问了if (instance == nullptr)判断instance为空,这个时候时间片结束了,线程B执行那么线程B也判断instance为空,那么就会new一个新的instance,再回到线程A的时候继续执行,就会创建另外一个新的instance,就违背了单例模式的初衷。这个时候就可以用锁或者是volatile关键字或者是atomic原子操作类模板。
假如我们使用的是锁的机制来确保线程安全的话第一种getInstance方法就是:
static Singleton* getInstance() {
// 双重检查锁定模式,减少锁的使用
std::lock_guardstd::mutex lock(mutex);
if (instance == nullptr) {
instance = new Singleton();
}
return instance;
}
我们在判断instance为空前加锁,这个时候的确可以确保线程安全,但是开销太大,此时这个线程结束了,那第二个线程进来获取instance的时候就会阻塞拿不到锁,导致整体的效率变低。
第二种方法使用volatile关键字来修饰我们的volatile static Singleton* instance ,这种方法是让编译器不要进行优化,每次使用实例的时候都从内存里面去取出来,而不是使用存在寄存器内的副本。但是他不能保证复合操作的原子性。
第三种就是双检查锁相比于第一种的检查锁,我们在判断条件之后进行加锁:
static Singleton* getInstance() {
// 双重检查锁定模式,减少锁的使用
if (instance == nullptr) {
std::lock_guardstd::mutex lock(mutex);
if (instance == nullptr) { // 再次检查,确保线程安全
instance = new Singleton();
}
}
return instance;
}
为什么要判断两次呢? 因为第一次如果线程A和线程B都进入了这行代码都判断为空,然后线程A拿到锁创建了新的instance返回,线程A结束了。但是线程B已经判断为空,B开始执行就去拿锁,拿到锁就直接创建一个新的instance没有判断是不是已经有其他线程创建了新的对象。这个时候加上二次判断就可以保证返回的只有一个实例。
但是这样也会造成一个问题也就是:内存读写reorder问题
这个问题是什么呢?其实很简单。
在我们的内存创建一个新的对象的时候,是首先在内存开辟新的内存空间,然后调用构造函数,然后在返回内存地址给一个指针。这里分为三步走,但是reorder就有可能会把第二步和第三步交换。先返回一个内存地址,但是这个时候还没有进行第三步构造函数还没调用呢,成员变量都没初始化是一个空的内存空间。这个时候就容易出现问题,有的线程判断了instance为空,但是拿到的对象其实是空的没有初始化。也就被编译器给欺骗了。那我们该如何解决这个问题呢?在之前遇到这个问题是没有办法的,但是C++11之后引入了std::atomic模板类我们就可以解决这个问题,下面是新的getInstance方法的代码:
std::atomic<Singleton*> Singleton::m_instance;
std::mutex Singleton::m_mutex;
Singleton* Singleton::getInstance() {
Singleton* tmp = m_instance.load(std::memory_order_relaxed);
//确保内存不会被reorder优化
std::atomic_thread_fence(std::memory_order_acquire);//获取内存fence
if (tmp == nullptr) {
std::lock_guardstd::mutex lock(m_mutex);
tmp = m_instance.load(std::memory_order_relaxed);
if (tmp == nullptr) {
tmp = new Singleton;
//配套使用
atomic_thread_fence(std::memory_order_release);//释放内存fence
m_instance.store(tmp, std::memory_order_relaxed);
}
}
return tmp;
}
这个时候就是一个真正的线程安全的单例模式。