在C++编程里,单例是比较常用的一个模式,因为单例用起来兼具C++面向对象性,C面向过程性的爽感,它还有两个很受欢迎的恶魔级特性,
1)不需关心创建时机(因为在进入main函数之前已经初始化了)
2)不需显式回收(因为退出main过程后自动回收全局对象)
但程序里有过多的单例,会影响程序的启动速度且增加内存占用,所以一般会采用懒单例
模式替代之,即在需要使用的时候创建它,如下所示:
template<class T>
class Singlton{
public:
static T& GetInstance(){
if(m_pInstance == NULL){
m_pInstance = new T;
}
return *m_pInstance;
}
private:
static T * m_pInstance;
};
template<class T>
T* Singlton<T>::m_pInstance = NULL;
这个单例类虽然看起来简洁明了,非常优美(先忽略它会在构造或析构时会带来什么问题),
但由于单例本质上是一个全局对象,采用上述单例,在多线程下是非常有问题的:
1)当Thread1通过判断(红色部分)进入if语句块执行申请内存操作,由于new操作是非原子
操作(非线程安全),且会切换到内核模式。
2)当Thread2进入if判断时,Thread1的内存分配还未完成(m_pInstance没被赋值,仍然
为NULL),Thread2也会执行申请内存操作。
3)当两者完成内存申请操作后,都会去给m_pInstance赋值,这会导致有一方数据被冲掉,
出现数据一致性问题。
好了,现在看一下线程安全的懒单例类(出自google源码):
template <typename Ty_>
class LazySingleton {
public:
static Ty_& GetInstance(){
1: while(me_ == NULL || me_ == (void*)-1){
2: PVOID result = InterlockedCompareExchangePointer((PVOID*)&me_, -1, NULL);
3: if(*(PVOID*)&me_ == -1){
4: Ty_* new_instance = new Ty_();
5: InterlockedCompareExchangePointer((PVOID*)&me_, (PVOID)new_instance, -1);
}
}
6: return *const_cast<Ty_*>(me_);
}
private:
static volatile Ty_ *me_;
};
template <typename Ty_>
volatile Ty_* LazySingleton<Ty_>::me_;
首先介绍一下上述类中两个生僻用法:
a)关键字volatile,这个网上很多说法,如果在内存某处读取一个变量到寄存器进行操作,
如果变量有任何变化,都会立即反应到内存中,而不会驻留在缓存(cache)中。
b)函数PVOID __cdecl InterlockedCompareExchangePointer(PVOID volatile *Destination,
PVOID Exchange,
PVOID Comparand)
是windows提供的一个比较并交换指针值的原子操作函数,它表示如果destination的值与Comparand值相等,则把Exchange的值赋给destination,如果不相等则什么都不做,
该函数提供完全的内存栅栏(barrier)来保证内存操作有序。
分析该类的线程安全性:
1) 如果对象已经创建,则不会进入while语句,直接返回对象引用,这种情况不存在线程安全性,接下来分析对象未创建情况。
2) 对象未创建(即me_== NULL),Thread1进入while循环,它首先通过第2行原子操作,如果me_==NULL,则me_=-1,否则什么都不做,即me_如果等于-1或其他值,直接进入
第3行,如果有多个线程都进入第2行对me_进行写操作不会出现多线程问题(此处是为了防止与第5步产生同步问题),如果Thread1执行完第2行,进入第3行之前,Thread2已经给me_
赋值,则会直接返回me_对象引用。
3) 我们主要分析下第4行,第5行代码,Thread1进入第4行,执行new操作,如果此时没有其他线程并行,则会顺利进入第5行,交换后me_为new_instance地址而不再为-1,这样其
它线程不再进入第3步,对象创建完成。如果有进程并行进入第4行,两个进程都创建了一个实例对象,但执行到第5行时,需要按序执行,最终只会有一个实例对象被赋值到me_,不会
引起数据不一致性问题。
结束语:在日常开发中一般不会有人写这么精细的懒单例类,因为即使是线程不安全类出问题的概率也很低,但这个类反应出多线程情况下一些复杂的问题需要我们关注。