单例模式主要要两种实现,一种是饿汉模式,另一种是懒汉模式。
饿汉模式就是在类一开始生成时就产生,而懒汉是要等到调用时,才生成。
下面分别讲解这两种模式。
懒汉
v1: not thread safe
class Singleton{
private:
static Singleton*instance;
Singleton(){
cout<<"Singleton ctor"<<endl;
}//let the ctor private
public:
static Singleton* getInstance(){
if(instance==NULL)
instance=new Singleton;
return instance;
}
};
Singleton* Singleton::instance=NULL;
v2 not thread safe
下面这种,与上面那种是等价的。
class Singleton{
private:
Singleton(){
cout<<"Singleton ctor"<<endl;
}
public:
static Singleton* getInstance(){
static Singleton instance; //is equal version1 .not thread safe
return &instance;
}
};
v3 thread safe
class Singleton{
private:
static Singleton*instance;
Singleton(){
cout<<"Singleton ctor"<<endl;
}
~Singleton(){
cout<<"Singleton dtor"<<endl;
}
class Garbo{
public:
~Garbo(){
if(Singleton::instance)
delete instance;
}
};
//static Garbo garbo;
public:
static mutex mutex_;
static void fun(){
delete instance;
}
static Singleton* getInstance(){
lock_guard<mutex> lock(mutex_);
if(instance==NULL){
instance=new Singleton;
atexit(fun);
}
return instance;
}
};
mutex Singleton::mutex_;
Singleton* Singleton::instance;
//Singleton::Garbo Singleton::garbo;
其实线程安全也就是在getInstance函数中加锁而已,但这里会有两个问题,第一个是这个加锁位置会导致每次访问getInstance都会锁住,导致效率较低,这个问题会在下面解决;
另外一个问题就是,当没有人会调用类时,怎么释放Singleton*指针?这里有两种方法解决,一种是如上程序中,将释放指针的函数传给ateixt,这样,在程序释放时,就会执行该函数,从而释放指针;还有一种解决方法是定义一个成员类,在销毁类的时候就会销毁成员类,我们只要在该成员类的析构函数中调用释放Singleton*指针的代码,就可以了。
为了解决上面提出的效率低下问题,我们引出下面这种,dcl(double check lock)
v3 not thread safe
class Singleton{
private:
Singleton(){
cout<<"ctor"<<endl;
}
static Singleton* instance;
public:
static mutex mtx;
static Singleton* getInstance(){
if(instance==NULL){ //1
lock_guard<mutex> lck(mtx);//2
if(instance==NULL){ //3
instance=new Singleton;//4
}
}
return instance;
}
};
Singleton* Singleton::instance;
mutex Singleton::mtx;
如果没有3的话,会导致一种情况,比如说,多个线程都通过了1,到达2,这时候只有一个能通过2,设为a线程,其他都阻塞在2,这时候这个通过的a线程继续执行4,然后出来,这时候,刚刚那些线程会有一个能通过2,设为b,因为刚刚进去的线程a已经出来,锁已经释放,所以b线程能够进入,如果没有3这个判断的话,线程b就又会重新构造一个Singleton。
这种double check lock 方法在很长一段时间被很多人认为是对的,只到有个大神发现了问题,这个问题就出在new上面
其实new这个关键字完成三件事
a 调用new 操作符,分配空间
b 调用类的构造函数
c 将指针返回
我们之前觉得double check lock是对的,是因为我们觉得new做这三件事按照上面顺序的,但是,这个其实是不一定的,并没有规定说,new 关键字要按上面顺序来,所以,加入顺序是acb的话,就会出现一个线程x在位于4这个位置时,有另一个线程y刚刚进来,位于1处,但是因为此时,new 关键字已经执行到c处,instance已经不是null了,所以线程y判断instance!=null,跨过2,直接返回给程序用,但是,其实这个时候instance是还没初始化的。
所以要解决这个问题,就有了下面这个版本
v thread safe
class Singleton{
private:
static Singleton*instance;
static pthread_once_t ponce;
Singleton(){
cout<<"Singleton ctor"<<endl;
}//let the ctor private
static void init(){
instance=new Singleton;
}
public:
static Singleton* getInstance(){
pthread_once(&ponce,init);
return instance;
}
};
Singleton* Singleton::instance=NULL;
pthread_once_t Singleton::ponce=PTHREAD_ONCE_INIT;
这种方法实际上是借助了linux下的pthread_once在多线程下只会执行一次的特性。
饿汉
class Singleton{
private:
Singleton(){
cout<<"Singleton ctor"<<endl;
}
static Singleton* instance;
public:
static Singleton* getInstance(){
return instance;
}
};
Singleton* Singleton::instance=new Singleton();
总结一下,单例模式懒汉模式getInstance函数必须为static,而且里面用到的东西,如fun函数,mutex都必须为static,然后c++ 单例最好的实现是利用pthread_once,这样就可以避免double check lock的异常和直接加锁的效率抵下。然后特别注意,c++成员中的static成员变量,类中的只是声明而已,并不是定义,所以必须在类外定义。