1. 单例模式(Singleton)定义
保证一个类仅有一个实例,并提供一个该实例的全局访问点。
2. 单例模式结构图
3. 单例模式几种实现方式(上代码)
class Singleton
{
private:
Singleton(); //构造函数私有(外界不能使用)
Singleton(const Singleton& other); //拷贝构造函数私有
public:
static Singleton* getInstance();
static Singleton* m_instance; //静态成员变量
};
Singleton* Singleton::m_instance = nullptr;
1. 非安全版本(适用于单线程)
Singleton* Singleton::getInstance()
{
if (m_instance == nullptr)
{
m_instance = new(std::nothrow) Singleton();
if (m_instance == nullptr)
{
return nullptr;
}
}
return m_instance;
}
2. 线程安全版本 读操作线程浪费
Singleton* Singleton::getInstance()
{
std::mutex mtx; //函数结束时锁资源释放
if (m_instance == nullptr)
{
m_instance = new(std::nothrow) Singleton();
if (m_instance == nullptr)
{
return nullptr;
}
}
return m_instance;
}
//3. 双检查锁(double check lock) 内存读写reorder不安全
Singleton* Singleton::getInstance()
{
if (m_instance == nullptr)
{
std::mutex mtx; //函数结束时锁资源释放
m_instance = new(std::nothrow) Singleton();
if (m_instance == nullptr)
{
return nullptr;
}
}
return m_instance;
}
问题分析:
我们默认或假定的构造顺序一般如下:
1). 分配内存
2). 调用构造器
3). 指针返回值给instance
实际reorder后的顺序可能为:
1. 分配内存
2. 指针返回值给instance
3. 调用构造器
导致的问题:
当一个线程执行到第二步时,假如此时有另外一个线程访问,会默认m_instance不为空返回,此时实际还未调用构造器,进而导致不可预知的问题。
4. c++11版本之后的跨平台实现(volatile)
std::atomic<Singleton*> Singleton::m_instance; //原子对象
std::mutex Singleton::m_mutex;
Singleton* Singleton::getInstance()
{
Singleton* s = m_instance.load(std::memory_order_relaxed); //屏蔽编译器的reorder
std::_Atomic_thread_fence(std::memory_order_acquire); //获取内存fence
if (s == nullptr)
{
std::lock_guard<std::mutex> lock(m_mutex);
s = m_instance.load(std::memory_order_relaxed); //取变量
if (s == nullptr)
{
s = new Singleton; //保证不出现reorder
std::_Atomic_thread_fence(std::memory_order_release); //释放内存fence
m_instance.store(s, std::memory_order_relaxed);
}
}
return s;
}
4. 总结
1) 单例模式中的实例构造器可以设置为protected以允许子类派生;
2) 单例模式一般不要支持拷贝构造函数和Clone接口,因为这有可能导致多个对象实例,违背单例模式设计初衷;
3) 注意对double check的正确实现;
5. 扩展
值得深究的点: volatile的内存屏障作用。
voldatile关键字具有“易变性”,声明为volatile变量编译器会强制要求读内存,相关语句不会直接使用上一条语句对应的的寄存器内容,而是重新从内存中读取。
其次具有”不可优化”性,volatile告诉编译器,不要对这个变量进行各种激进的优化,甚至将变量直接消除,保证代码中的指令一定会被执行,这一点可以与单例模式(double check存在问题)中reorder联系起来。