Singleton模式可能是最容易理解的一种程序设计模式了,实际上就使用频度来说也是相当高的,大规模的项目中能使用单例的地方比比皆是。它本身是工厂模式的一种特例,很多人觉得该模式很简单,但是,实际上,要基本正确的实现一个单例,却不见得那么容易,尤其是如果你用C++去实现。
我们就来一步步的实现一个可以用于项目的中的单例吧!
首先看看单例要满足哪些条件:
(1) 整个进程中单例对象只能有一份(否则还叫啥单例),那么它的构造方法就必须得是私有(或者保护,如果支持继承)的,这样外部既不能申明局部变量也不能在堆上动态创建;
(2) 它自身需要实例化自己一份,这是整个系统唯一的一份实例;
(3) 需要提供一个可以获得唯一实例的公共的外部接口,这样外部才能调用单例的方法。
1、 最简单的一个实现方式。
// -------------------------------------------------------------------------
class KSimpleSingleton
{
public:
static KSimpleSingleton& GetInstance();
static KSimpleSingleton* GetInstance2();
private:
KSimpleSingleton();
static KSimpleSingleton* psg;
};
// -------------------------------------------------------------------------
// $Log: $
#endif /* __KSIMPLESINGLETON_H__ */
// .cpp file
/* -------------------------------------------------------------------------
// 文件名 : KSimpleSingleton.cpp
// 创建者 : IUNKNOW
// 创建时间 : 2011/3/19 18:23:11
// 功能描述 :
//
// $Id: $
// -----------------------------------------------------------------------*/
#include "stdafx.h"
#include "KSimpleSingleton.h"
// -------------------------------------------------------------------------
KSimpleSingleton* KSimpleSingleton::psg = NULL;
KSimpleSingleton::KSimpleSingleton()
{}
KSimpleSingleton& KSimpleSingleton::GetInstance()
{
static KSimpleSingleton sg;
return sg;
}
KSimpleSingleton* KSimpleSingleton::GetInstance2()
{
if(!psg)
{
psg = new KSimpleSingleton();
}
return psg;
}
// -------------------------------------------------------------------------
// $Log: $
这里我懒了手脚,就把两种方法写在一个类里面了,一种是定义一个静态static变量,一种是在堆上new一个对象出来。这种实现简单是简单,但是问题也很严重,如果你的项目里面就一个主线程在跑着那这是没问题的(称得上项目如果只有一个线程简直是不可能的),但是如果遇到多线程并发的访问这个单例对象那一切都乱了,如果是用上面的GetInstance接口,几个线程同时调用到GetInstance(),对于static变量系统确实会保证只有一份,第一个真正得到机会初始化对象的线程会去正确的初始化对象,但是其他的线程会不等对象初始化完成直接去调用对象的对外公共接口。如果是GetInstance2接口,那更混乱,几个线程同时调用到GetInstance2(),那可能产生几个对象出来,单例也就不再单例了。
2、 使用临界区来解决线程同步问题
// -------------------------------------------------------------------------
class CritSect
{
public:
CritSect()
{
::InitializeCriticalSection(&_critSection);
}
~CritSect()
{
::DeleteCriticalSection(&_critSection);
}
class Lock
{
public:
Lock(CritSect& critSect):_critSect(critSect)
{
_critSect.Acquire();
}
~Lock()
{
_critSect.Release();
}
private:
CritSect& _critSect;
};
private:
void Acquire()
{
::EnterCriticalSection(&_critSection);
}
void Release()
{
::LeaveCriticalSection(&_critSection);
}
CRITICAL_SECTION _critSection;
};
// .cpp file
// -------------------------------------------------------------------------
// $Log: $
#endif /* __KCRITSECT_H__ */ :
#include "KCritSect.h"
// -------------------------------------------------------------------------
class KSecSingleton
{
public:
static KSecSingleton* GetInstance();
static KSecSingleton* GetInstance2();
private:
KSecSingleton();
static CritSect m_cs;
static KSecSingleton* psg;
};
// -------------------------------------------------------------------------
// $Log: $
#endif /* __KSECSINGLETON_H__ */
// .cpp file
/* -------------------------------------------------------------------------
// 文件名 : KSecSingleton.cpp
// 创建者 : IUNKNOW
// 创建时间 : 2011/3/20 22:32:46
// 功能描述 :
//
// $Id: $
// -----------------------------------------------------------------------*/
#include "stdafx.h"
#include "KSecSingleton.h"
// -------------------------------------------------------------------------
CritSect KSecSingleton::m_cs;
KSecSingleton* KSecSingleton::psg = NULL;
KSecSingleton::KSecSingleton()
{}
KSecSingleton* KSecSingleton::GetInstance()
{
CritSect::Lock lock(m_cs);
static KSecSingleton sg;
//psg = new KSecSingleton();
return &sg;
}
KSecSingleton* KSecSingleton::GetInstance2()
{
// double check
if(!psg)
{
CritSect::Lock lock(m_cs);
if(!psg)
{
psg = new KSecSingleton();
}
}
return psg;
}
// -------------------------------------------------------------------------
// $Log: $
这里同样定义了两个对外接口(实际使用中不要这样写,适需求一个就够了),使用了windows里面的临界区来进行线程同步,现在在线程并发的时候不会再出现1里面说到的混乱的情况了,需要注意的是,GetInstance2比GetInstance还有点点不同,那就是GetInstance2貌似多了一次空指针的判断……多余的?NONONO,这就是传说中的double check(中文术语:双重检测,貌似是来之java中),多的最外层的这个检测的目的是为了在对象已经建立后,后面再调用的时候节省一个同步的操作,因为已经没必要同步了。
3、基本正确的实现,其实上面还有一个问题,就是如果你在堆上把单例new了出来,是没有去释放delete它的,可以在主线程的最后调用delete KSecSingleton::GetInstance2()来做到,但是有点恶。其实可以借助C++的特性用智能指针来帮助完成,看下面这个模板的例子。
#include <memory>
using namespace std;
#include "KCritSect.h"
// -------------------------------------------------------------------------
template <class T>
class KSingleton
{
public:
static T* GetInstance();
protected:
KSingleton(){}
KSingleton(KSingleton&){}
KSingleton& operator=(KSingleton&){}
virtual ~KSingleton(){}
static CritSect m_cs;
static auto_ptr<T> m_pInstance;
};
template <class T>
CritSect KSingleton<T>::m_cs;
template <class T>
auto_ptr<T> KSingleton<T>::m_pInstance;
template <class T>
T* KSingleton<T>::GetInstance()
{
// double check
if(!m_pInstance.get())
{
CritSect::Lock lock(m_cs);
if(!m_pInstance.get())
{
m_pInstance.reset(::new T);
}
}
return m_pInstance.get();
}
// -------------------------------------------------------------------------
// $Log: $
#endif /* __KSINGLETON_H__ */
使用方法,从模板类KSingleton继承即可:
其实上面第三种方法还是有瑕疵的,就是同步对象使用的是windows的临界区,不能够跨平台,如果想跨平台,可以尝试使用boost库中的线程同步对象。(boost还是很不错的,建议以后多使用)