单例模式(Singleton)是指一个类仅有一个实例对象,并且该类提供一个获得该实例对象的全局访问点,它包含三个关键元素:
元素一:提供private类型的构造函数
元素二:提供private类型的的静态成员变量,以保存唯一的实例对象;
元素三:提供获得本类实例的全局访问点GetInstance函数,它是静态类型的
初级版本
根据这三个关键元素可以写出一个基本的单例模式,其代码如下:
class CSinglton
{
private:
//(1)私有额构造函数
CSinglton(){}
//在析构函数中释放实例对象
~CSinglton()
{
if (pInstance != NULL)
{
delete pInstance;
pInstance = NULL;
}
}
public:
//(3)获得本类实例的唯一全局访问点
static CSinglton* GetInstance()
{
//若实例不存在,则创建实例对象
if (NULL == pInstance)
{
pInstance = new CSinglton();
}
//实例已经存在,直接该实例对象
return pInstance;
}
private:
static CSinglton* pInstance;//(2)唯一实例对象
};
//静态成员变量,类外初始化实例对象
CSinglton* CSinglton::pInstance = NULL;
由于CSinglton类的构造函数是私有的,外界无法创建实例对象,只能由类本身来创建;这里由GetInstance()全局访问点来创建唯一的实例对象;
下面我们可以创建测试代码,判断是否符合预期,测试代码如下:
CSinglton *pInstance1 = CSinglton::GetInstance();
CSinglton *pInstance2 = CSinglton::GetInstance();
if (pInstance1 == pInstance2)
{
cout << "同一个实例" << endl;
}
else
{
cout << "是两个实例" << endl;
}
运行结果:
从测试结果可以看出,代码运行符合基本预期;
多线程基础版本
如果CSinglton 类运行在多线程环境下,可能会导致该类创建两个实例,并造成内存泄漏,原因分析如下:
比如在线程A和线程B中均第一次调用GetInstance函数,当线程A开始执行new CSinglton()语句时,线程A暂停执行;而此刻线程B获得执行权利,并成功创建了实例对象B;当线程A又获得CPU时间片,则继续执行new CSinglton()语句创建实例对象A,导致实际中创建了两个实例对象,并且实例B的指针值被实例A覆盖,实例B没有被释放,导致内存泄漏;
因此我们可以改进我们的代码,对GetInstance内部增加同步机制,修改代码如下:
class CSinglton
{
private:
//(1)私有额构造函数
CSinglton(){}
~CSinglton()
{
if (pInstance != NULL)
{
delete pInstance;
pInstance = NULL;
}
}
public:
//(3)获得本类实例的唯一全局访问点
static CSinglton* GetInstance()
{
//利用MFC中的CSingleLock完成同步,只有一个线程会进入
CSingleLock singleLock(&m_CritSection);
singleLock.Lock();
if (NULL == pInstance)
{
//若实例不存在,则创建实例对象
pInstance = new CSinglton();
}
singleLock.Unlock();
return pInstance;
}
private:
static CSinglton* pInstance;//(2)唯一实例对象
static CCriticalSection m_CritSection;
};
//静态成员变量,类外初始化实例对象
CSinglton* CSinglton::pInstance = NULL;
CCriticalSection CSinglton::m_CritSection;
当一个线程已经位于临界区时,另一个线程将会被阻塞,直到当前线程退出临界区,另一个线程才会进入,就可以解决多线程同步问题;
多线程高效版
在一般的多线程环境下,以上代码就可以保证正确性,但是在高并发、访问量很大的环境下,上述的代码实现将对性能造成很大影响;因为每次调用GetInstance都需要加锁,解锁,比较浪费时间,解决方案是采用双重锁定。GetInstance多线程高效版代码如下:
static CSinglton* GetInstance()
{
//仅在实例未被创建时加锁,其他时候直接返回
if (NULL == pInstance)
{
CSingleLock singleLock(&m_CritSection);
singleLock.Lock();
if (NULL == pInstance)
{
//若实例不存在,则创建实例对象
pInstance = new CSinglton();
}
singleLock.Unlock();
}
//实例已经存在,直接该实例对象
return pInstance;
}
以上代码实现过程叫做“Double-Check-Locking(双重锁定)”,即我们不让线程每次都加锁,而仅在实例未被初始化时,才进行线程同步,不仅保证内存不泄漏,还大大提高访问性能;
静态初始化版本
这种版本不仅实现最简单而且还避免多线程下的不安全性,其代码如下:
class CSinglton
{
private:
CSinglton(){}
~CSinglton()
{
if (pInstance != NULL)
{
delete pInstance;
pInstance = NULL;
}
}
public:
//获得本类实例的唯一全局访问点
static CSinglton* GetInstance()
{
return pInstance;
}
private:
static CSinglton* pInstance;
};
//程序运行初期就创建实例
CSinglton* CSinglton::pInstance = new CSinglton;
饿汉式和懒汉式单例类
在单例模式中,常常提到了饿汉式单例类和懒汉式单例类两个词,上面提到的“初级版本”“多线程基础版本” “多线程高效版”都是懒汉式单例类,而“静态初始化版本”是饿汉式单例类,它们的区别主要如下:
- 饿汉式单例就是在程序运行之前就创建实例,不管最终程序中是否用到,都占用资源,形容饥不择食的模样;
- 懒汉式单例就是程序在第一次调用全局访问点时才实例对象;
- 饿汉式单例不需要考虑多线程的安全性,但是有可能导致资源浪费,懒汉式单例需要采用“双重锁定”才能保证系统性能和正确性;
采用何种方式,我们应该根据实际应用情况区别对待;
参考文章: