单例模式
含义:单例模式从含义来讲,就是只能有一个实例,一个实例的意思就是通过一个类只能创建一个对象。
分类:单例模式可以分为懒汉式和饿汉式。
懒汉式
:只有需要的时候才创建对象(对象就是实例),因此当有多个线程同时需要该对象实例时,就可能存在线程安全的问题,导致最终可能跟创建多个对象,此时也就不是单例模式。
饿汉式
:在开始时(编译期)就会就会创建对象,因此这个时候不存在线程安全的问题。
具体实现
对于单例模式,如果只能允许有一个对象,那么首先就应该把构造函数声明为私有的,并且不允许该单例进行拷贝,赋值,故一个单例类的话那么就需要禁用点该类的拷贝构造,拷贝赋值等方法。
饿汉式
代码一
class sigleton {
sigleton() {}
sigleton(sigleton&) = delete;
sigleton& operator=(sigleton&) = delete;
public:
static sigleton& getInstance() {
static sigleton s;
return s;
}
};
解析:对于该单例类的实现,看起来有问题吗?直接看起来好像没什么问题,不过当我们实例化一个对象时,我们再继续看一下,代码如下:
解析:从上图,我们可以看出,当我们想获得该单例对象时,发现报错了。并且从上图我们可以看出,提示说sigleton::sigleton(sigleton &)为不可访问。此处有一个点需要注意的就是我们在单例类内部定义的是一个静态变量,静态变量在初始化时只会初始一次,并且我们返回的是一个引用,故不会生成一个临时的对象实例。那么显然问题就出在第19行,我们应该很容易就会发现我们此处是直接将获得的单例对象赋值给一个我们定义的变量,那么很明显此时会通过我们返回的单例对象来拷贝构造一个新的对象,可是我们已经禁用了拷贝构造函数,因此就会出现上图所示的错误。既然知道了这个错误,那么很容易解决,即我们将该变量声明为一个引用,自然就会解决这个问题。如下图:
#include <iostream>
#include <memory>
using namespace std;
class sigleton {
sigleton() {}
sigleton(sigleton&) = delete;
sigleton& operator=(sigleton&) = delete;
public:
static sigleton& getInstance() {
static sigleton s;
return s;
}
};
int main() {
auto& tmp = sigleton::getInstance();
return 0;
}
基于这个单例模式,我们来验证一下,是否真的只能创建一个对象,验证如下:
从上图可以看到,确实只能创建一个对象,这个单例模式初步符合我们的预期。但是有一个问题就是,该单例模式只能通过引用来创建对象,如果不是引用的话就创建不了对象。因此我们 需要改进一下,那么改进的思路就是我们在外部不能直接调用构造函数(包括拷贝构造),那么不调用构造函数的方法就只有两种,一种是引用,一种是指针,我们上面是通过引用来实现的,可是还不够完美,那么我们的改进思路就是通过指针来完善。代码如下:
#include <iostream>
#include <memory>
using namespace std;
class sigleton {
sigleton() {}
sigleton(sigleton&) = delete;
sigleton& operator=(sigleton&) = delete;
public:
static sigleton* getInstance() {
static sigleton s;
return &s;
}
};
int main() {
sigleton* tmp = sigleton::getInstance();
return 0;
}
如山图,只修改了很少的一部分,即改成了通过指针进行调用。
懒汉式
初版代码
#include <iostream>
#include <memory>
using namespace std;
class sigletonLazy {
sigletonLazy() {}
sigletonLazy(sigletonLazy&) = delete;
sigletonLazy& operator=(sigletonLazy&) = delete;
public:
static sigletonLazy* getInstance() {
m_instance = new sigletonLazy;
return m_instance;
}
private:
static sigletonLazy* m_instance;
};
sigletonLazy* sigletonLazy::m_instance = nullptr;
int main() {
sigletonLazy* tmp = sigletonLazy::getInstance();
return 0;
}
对于这个初版代码,直接看我相信大多数人都能看出来其中的问题。问题就是对于每次调用getInstance()获取对象时,都会重新创建一个新的对象,从而导致创建了多个对象,也就导致了单例模式的失效。我们验证一下。
从该图的最后,我们直接输出了地址可以看出两个对象的地址都不相同,显然是两个对象。基于此,我们需要让该对象只创建一次,因此需要下面这种做法:
对于这个上面这个代码,我们只是简单的做了一下修改,即当实例指针非空时才创建对象,否则直接返回。这个代码直接看貌似没有什么问题,已经把构造函数声明为私有,并且禁用掉了拷贝构造和拷贝赋值。然而,这个代码同样存在问题,第一是内存泄漏的问题,因此我们的析构函数时私有的,故不能直接调用到析构函数,并且我们申请的对象空间是在堆里进行申请的,故如果内存不进行释放就会存在内存泄漏的问题。此外,还会出现关于多线性的的并发问题,当有多个线程同时调用getInstance()获取实例对象时,有可能会同时进入到if语句内部,从而造成创建多个对象。基于此,我们需要进行加锁并处理内存泄漏:
#include <iostream>
#include <memory>
#include <mutex>
using namespace std;
class sigletonLazy {
sigletonLazy() {}
sigletonLazy(sigletonLazy&) = delete;
sigletonLazy& operator=(sigletonLazy&) = delete;
~sigletonLazy() {
}
static void Destructor() {
if (m_instance) {
delete m_instance;
m_instance = nullptr;
}
}
public:
static sigletonLazy* getInstance() {
m_mutex.lock();
if (!m_instance) {
m_instance = new sigletonLazy;
atexit(Destructor);
}
m_mutex.unlock();
return m_instance;
}
private:
static sigletonLazy* m_instance;
static mutex m_mutex;
};
sigletonLazy* sigletonLazy::m_instance = nullptr;
mutex sigletonLazy::m_mutex;
int main() {
sigletonLazy* tmp = sigletonLazy::getInstance();
return 0;
}
对于该懒汉式的实现,确实没有什么问题了,但是不够完美,因为一上来就直接进行加锁,那么回导致效率太低。因此为了提升效率,会进行双重检测,也即双重监测锁,如下:
#include <iostream>
#include <memory>
#include <mutex>
using namespace std;
class sigletonLazy {
sigletonLazy() {}
sigletonLazy(sigletonLazy&) = delete;
sigletonLazy& operator=(sigletonLazy&) = delete;
~sigletonLazy() {
}
static void Destructor() {
if (m_instance) {
delete m_instance;
m_instance = nullptr;
}
}
public:
static sigletonLazy* getInstance() {
if (!m_instance) {
m_mutex.lock();
if (!m_instance) {
m_instance = new sigletonLazy;
atexit(Destructor);
}
m_mutex.unlock();
}
return m_instance;
}
private:
static sigletonLazy* m_instance;
static mutex m_mutex;
};
sigletonLazy* sigletonLazy::m_instance = nullptr;
mutex sigletonLazy::m_mutex;
int main() {
sigletonLazy* tmp = sigletonLazy::getInstance();
return 0;
}
对于上述代码,其实还是存在问题,即对于语句m_instance = new sigletonLazy,该一条语句其实是分为三部分:(1)分配资源 (2)初始化构造函数 (3)指针赋值;由于编译器内部存在编译优化,指令重排,因此这三条语句的执行实际上不确定是按照何种顺序执行,因此当线程A进入了临界区,并执行了指令(1),(3)后,线程A的时间片到期了,那么此时就会调度到另一个线程,如果另一个线程B同样获取该实例对象,在判断时发现对应的实例对象非空,直接返回。这时,问题就出现了,即此时(2)还没有执行,即该对象还没有进行初始化,因此当使用该对象时会发生错误,而且很难察觉到。基于此,有了内存屏障来解决该问题,代码如下:
#include <iostream>
#include <memory>
#include <mutex>
#include <atomic>
using namespace std;
class sigletonLazy {
sigletonLazy() {}
sigletonLazy(sigletonLazy&) = delete;
sigletonLazy& operator=(sigletonLazy&) = delete;
~sigletonLazy() {
}
static void Destructor() {
sigletonLazy* tmp = m_instance.load(memory_order_relaxed);
if (tmp) {
delete tmp;
}
}
public:
static sigletonLazy* getInstance() {
sigletonLazy* tmp = m_instance.load(memory_order_relaxed);
atomic_thread_fence(memory_order_acquire);//获取内存屏障
if (!tmp) {
m_mutex.lock();
tmp = m_instance.load(memory_order_relaxed);
if (!tmp) {
tmp = new sigletonLazy;
atomic_thread_fence(memory_order_release);//释放内存屏障
m_instance.store(tmp, memory_order_relaxed);
atexit(Destructor);
}
m_mutex.unlock();
}
return tmp;
}
private:
static atomic<sigletonLazy*> m_instance;
static mutex m_mutex;
};
atomic<sigletonLazy*> sigletonLazy::m_instance = nullptr;
mutex sigletonLazy::m_mutex;
int main() {
sigletonLazy* tmp = sigletonLazy::getInstance();
return 0;
}
对于这版代码,其实还可以优化,即可以使用模板来进行提高单例模式的复用行,代码如下:
#include <iostream>
#include <memory>
#include <mutex>
#include <atomic>
using namespace std;
template <class T>
class sigletonLazy {
sigletonLazy(sigletonLazy&) = delete;
sigletonLazy& operator=(sigletonLazy&) = delete;
static void Destructor() {
T* tmp = m_instance.load(memory_order_relaxed);
if (tmp) {
delete tmp;
}
}
protected:
sigletonLazy() {}
virtual ~sigletonLazy() {}
public:
static T* getInstance() {
T* tmp = m_instance.load(memory_order_relaxed);
atomic_thread_fence(memory_order_acquire);//获取内存屏障
if (!tmp) {
m_mutex.lock();
tmp = m_instance.load(memory_order_relaxed);
if (!tmp) {
tmp = new T;
atomic_thread_fence(memory_order_release);//释放内存屏障
m_instance.store(tmp, memory_order_relaxed);
atexit(Destructor);
}
m_mutex.unlock();
}
return tmp;
}
private:
static atomic<T*> m_instance;
static mutex m_mutex;
};
template<class T>
atomic<T*> sigletonLazy<T>::m_instance = nullptr;
template<class T>
mutex sigletonLazy<T>::m_mutex;
class ChildSigleton : public sigletonLazy<ChildSigleton> {
friend class sigletonLazy;
public:
/**
* @method1
* @method1
* @......
* @methodn
*/
private:
ChildSigleton() {}
~ChildSigleton(){}
ChildSigleton(ChildSigleton&) = delete;
ChildSigleton& operator =(ChildSigleton&) = delete;
};
int main() {
ChildSigleton* tmp = sigletonLazy<ChildSigleton>::getInstance();
return 0;
}
总结
单例模式实现过程
1 对于单例模式,不管是懒汉式还是饿汉式,必须要做的工作有,将构造和析构函数声明为私有(或者保护),禁用掉拷贝构造和拷贝复制函数。
2 不管是懒汉式还是饿汉式,对于提供的获取实例的方法,可以返回一个引用,也可以返回一个指针。如果是通过引用返回,那么最终接收时必须通过引用来接收,不然就会调用到被禁用的拷贝构造产生错误。如果是通过指针返回,那么就需要一个指针进行接收。
3 对于饿汉式来讲,不需要定义一个静态成员属性,可以直接在获取实例方法时定义一个静态成员属性;对于懒汉式来说,由于是要在使用时才能进行创建对象,故需要定义一个静态成员属性。
4 为了提高代码的复用性,可以通过一个模板来提高单例对象代码的复用性,使所有的单例类都继承该单例类对象。
对于懒汉式
和饿汉式
具体的实现会有区别,懒汉式由于是在需要的时候才创建对象,因此会出现线程安全的问题,而饿汉式是一开始在编译时就创建对象,故不存在线程安全的问题。
懒汉式
1 由于懒汉式需要维护一个指针,在堆上分配内存,故需要考虑内存泄漏的问题。
2 由于析构函数被声明为私有的,故不能在外部调用析构函数,需要提供一个释放资源的方法。
3 因此,为了避免内存泄漏,可以使用atexit(fun)函数,来在进程退出时调用该释放资源的方法释放资源。
饿汉式
1 不需要维护指针,故只需要实现一个实例获取方法即可