动机
在软件系统中,经常有这样一些特殊的类,必须保证它们在系统中只存在一个实例,才能确保它们逻辑正确性和良好的效率。
思考:如何绕过常规的构造器,提供一种机制来保证一个类只有一个实例?
头文件:
#include <iostream>
#include <mutex>
#include <atomic>
class Singleton
{
private:
Singleton();
Singleton(const Singleton& other);
public:
static Singleton* getInstance();
static Singleton* m_Instance;
};
实现文件:
#include "Singleton.h"
Singleton* Singleton::m_Instance = nullptr;
//V1
Singleton* Singleton::getInstance()
{
if (m_Instance == nullptr)
{
m_Instance = new Singleton();
}
return m_Instance;
}
//V2
Singleton* Singleton::getInstance()
{
m_mutex.lock();
if (m_Instance == nullptr)
{
m_Instance = new Singleton();
}
m_mutex.unlock();
return m_Instance;
}
//V3
Singleton* Singleton::getInstance()
{
if (m_Instance == nullptr)
{
m_mutex.lock();
if (m_Instance == nullptr)
{
m_Instance = new Singleton();
}
m_mutex.unlock();
}
return m_Instance;
}
上述代码讲到了三种构造函数实现的方式,以上三种方式皆存在部分问题,问题分别如下:
-
V1版本:
线程非安全版本,意思是当存在多个线程同时访问的话,无法保证该对象实例仅产生一个,当有2个线程threadA和threadB同时执行到if判断代码,皆因为当前是nullptr而进入new的逻辑,此时就可能产生多个对象实例,如果仅在单线程环境下则无问题。 -
V2版本:
线程安全版本,但锁的代价较高,意思是该方法能够保证只产生一个对象实例,但是当对象实例已经存在的时候,后续执行仅是读取操作依然会先上锁,这种锁其实是多余没必要的。 -
V3版本:
双检查锁,但由于内存读写reorder仍存在问题,从代码上看先判断当前是否存在对象实例,有则返回,没有再上锁进行第二次判断,这里第二次判断不能省略,因为在多线程情况下,比如threadA和threadB同时执行到第一个if判断里,threadA获得了锁,当threadA创建实例结束解开锁时,threadB也依然会额外创建实例,所以这里的第二个判断不能省。
但这种情况后来经大佬指出会存在内存读写reorder问题,会导致双检查锁的失效,正常流程是先分配内存,再通过构造对这片内存初始化,之后再把这片内存地址返回出去。但经过reorder后可能会出现分配内存后直接返回内存地址的情况,跳过了中间构造环节,这样threadB进入后判断当前不是nullptr就直接返回了,但此时获取到的地址是不能使用的。
推荐用法:
std::atomic<Singleton*> Singleton::m_Instance;
std::mutex Singleton::m_mutex;
Singleton* Singleton::getInstance()
{
Singleton* tmp = m_Instance.load(memory_order_relaxed);
std::_Atomic_thread_fence(std::memory_order_acquire);
if (tmp == nullptr)
{
std::lock_guard<std::mutex> lock(m_mutex);
tmp = m_Instance.load(memory_order_relaxed);
if (tmp == nullptr)
{
tmp = new Singleton();
std::_Atomic_thread_fence(std::memory_order_release);
m_Instance.store(tmp, std::memory_order_relaxed);
}
}
return tmp;
}