单例模式是一种常见的软件设计模式。在它的核心结构中只包含一个被称为 单例类的特殊类,通过单例模式可以保证系统中一个类只有一个实例而且该实例易于外界访问。
应用场景:对于系统中的某些类来说,只有一个实例很重要,例如一个系统中可以存在多个打印进程,但是只能有一个正在工作的打印进程;
根据维基百科对单例模式的描述:
确保一个类只有一个实例,并提供对该实例的全局访问。
那么,我们可以从中得到一个单例模式很重要的特征:
一个类只有一个对象。
一、单线程模型:
对于一个普通的类,我们可以通过构造函数随意生成N个对象,显然为了使一个类只能有一个对象,那么我们第一步应该先把构造函数设置为私有函数,禁用其复制与赋值能力。于是有了以下的代码:
class Singleton { private: Singleton(){ }; };
这样我们在main函数中就不能生成任何Singleton对象;
Singleton s;//error
那么我们该怎么解决这一问题呢?
这里我们根据static关键字的性质,绕过构造函数为private的限制,于是代码如下;
class Singleton { public: static Singleton *getInstance()//static 静态存储区 { return new Singleton; } //new 类型 private: Singleton(){ }; };
在main函数中这样调用:
int main(int argc, const char *argv[]) { cout << Singleton::getInstance() << endl; cout << Singleton::getInstance() << endl; return 0; }
我们发现打印出的两个对象的首地址是不一样的,也就是说,两次Singleton::getInstance()函数的返回值是不同的,因为,这个函数每次为为我们new一个新的对象。
以上的代码产生的问题是 无法保证产生的对象唯一。
那么我们又该如何解决这一问题呢?
在函数成员中我们设置一个static指针,其初始值为空,自然这个指针为本类所拥有,不属于某一个具体对象,通过每次判断该指针是否为空,说明是否已经产生了一个对象。代码如下:
//多线程下有隐患 class Singleton { public: static Singleton *getInstance()//static 静态存储区 { if(pSingleton == NULL) pSingleton = new Singleton; //new 类型 return pSingleton; } private: Singleton(){ }; static Singleton* pSingleton; }; Singleton *Singleton::pSingleton = NULL;//静态成员函数的定义
这样我们再用上面的main函数测试本段代码,结果说明值产生了一个对象。这说明我们实现了一个简单的单例模式。
二、多线程下的单例模式:
上段代码是否真的没有问题了呢?
显示不是的,我们考虑一个进程中含有多个线程的情形,代码如下:
#include <iostream> #include "Thread.h" #include <stdlib.h> using namespace std; //多线程隐患 class Singleton { public: static Singleton *getInstance()//static 静态存储区 { if(pSingleton == NULL) { ::sleep(1); //以防止内核速度过快 pSingleton = new Singleton; //new 类型 } return pSingleton; } private: Singleton(){ } static Singleton* pSingleton; }; Singleton *Singleton::pSingleton = NULL; class MyThread:public Thread { public: void run() { cout << Singleton::getInstance() << endl; cout << Singleton::getInstance() << endl; } }; //本例测试了多线程隐患 int main(int argc, const char *argv[]) { //Singleton(); const int Ksize = 12; MyThread threads[Ksize]; int i ; for ( i = 0; i != Ksize; i++) { threads[i].start(); } for ( i = 0; i != Ksize; i++) { threads[i].join(); } cout <<"hello"<< endl; return 0; }
测试结果如下:
0xb1300468 0xb1300498 0x9f88728 0xb1300498 0xb1300478 0xb1300498 0xb1100488 0xb1300498 0xb1300488 0xb1300498 0xb1300498 0xb1300498 0x9f88738 0xb1300498 0x9f88748 0xb1300498 0xb1100478 0xb1300498 0xb1100498 0xb1300498 0xb1100468 0xb1300498 0xb11004a8 0xb11004a8
我们发现,地址有很多是不同的。那么以上问题是怎么产生的呢?
因为,上面的进程中不止有一个线程,当有多个线程启动时,假设代号为A/B;当线程 A调用run函数时会在执行if(pSingleton==NULL)语句后停留一秒,这时B也会执行到if(pSingleton==NULL)语句,紧接着B会判断pSingleton是否为空,这时由于A还未new一个对象,所以B会进入到if语句的函数体中,所以此时A,B各会new一个新对象。所以就产生了以上结果。
那么又该如何解决这一问题呢?
很自然的我们会想到引进互斥锁,这样,就形成了以下代码:
//其余代码均未改动 class Singleton { public: static Singleton *getInstance() { mutex_.lock(); if(pInstance_ == NULL) { ::sleep(1); pInstance_ = new Singleton; } mutex_.unlock(); return pInstance_; } private: Singleton(){ } static Singleton *pInstance_; static MutexLock mutex_; }; Singleton *Singleton::pInstance_ = NULL; //static成员的定义 MutexLock Singleton::mutex_;
显然,通过测试,证明此方法是可行的。但是这里存在着一个很严重的问题:效率问题。
互斥锁会极大的降低系统的并发能力,因为每次调用都要加锁解锁操作。
于是我们写了以下测试:
class TestThread : public Thread { public: void run() { const int kCount = 1000 * 1000; for(int ix = 0; ix != kCount; ++ix) { Singleton::getInstance(); } } }; int main(int argc, char const *argv[]) { //Singleton s; ERROR int64_t startTime = getUTime(); const int KSize = 100; TestThread threads[KSize]; for(int ix = 0; ix != KSize; ++ix) { threads[ix].start(); } for(int ix = 0; ix != KSize; ++ix) { threads[ix].join(); } int64_t endTime = getUTime(); int64_t diffTime = endTime - startTime; cout << "cost : " << diffTime / 1000 << " ms" << endl; return 0; } //测试时间 int64_t getUTime() { struct timeval tv; ::memset(&tv, 0, sizeof tv); if(gettimeofday(&tv, NULL) == -1) { perror("gettimeofday"); exit(EXIT_FAILURE); } int64_t current = tv.tv_usec; current += tv.tv_sec * 1000 * 1000; return current; }
上面的代码中,我们开了 100 个线程,每个线程 调用1M次Singleton::getInstance()
测试结果如下:
cost : 6729 ms
什么方法可以解决这一问题?
采用双重锁模式
再次改进我们的代码:
class Singleton { public: static Singleton *getInstance() { if(pInstance_ == NULL) { mutex_.lock(); if(pInstance_ == NULL) //线程的切换 pInstance_ = new Singleton; mutex_.unlock(); } return pInstance_; } private: Singleton() { } static Singleton *pInstance_; static MutexLock mutex_; };
可以看到我们在getInstance中采用了 双重检查模式,这样的好处是什么呢?
安全性:内部已经采用互斥锁,代码无论如何都是安全的。
效率问题:当产生一个实例之后,pInstance就已不为空,后面每个线程访问到最外面的if判断就直接返回,并没有执行后面的加锁解锁操作
我们再次测试,结果如下:
cost : 458 ms
我们看到仅仅多了三行代码,却带来了十几倍的效率提升!!
Thread.h&Thread.cpp可参考:Linux组件封装之三:Thread
MutexLock.h&MutexLock.cpp可参考:Linux组件封装之一:MUtexLock