vlambda博客
学习文章列表

不是你记忆中的单例模式,但适用的程度,更胜一筹




故事线

我有一个好朋友广军,为了体验生活,在学校旁边盘了个店面,开了家奶茶汉堡店。
店面新开张,首先要做的事儿就是去工商局登记一下,然后做个章。

到了工商局,人家给他来了个友情提示:XXX只为私密文件,请不要给别人乱玩,出了啥事儿你需要负责。

登记完,做了章,做个店就算是开起来了,他开始当上了店长,刚开始事情多啊,他又要招聘,又要采购,又要宣传,又要会计算账···
但是这些事儿也只能他来干,亲力亲为,先把这个店扶起来。



单例模式

那这个故事就很好的契合了单例模式的应用场景,所以我这个朋友想和你聊聊单例模式。

什么是单例模式呢?
在项目中,有些类是需要对它们进行“计划生育”的,即这个类只能有一个实例,如果出现多个实例则会有数据不一致的风险。
你说我开个店,要是有两个老板,那今天又个单,我说签,他又跟人家说不签;这个员工不积极,我说开,他又说不开···那岂不是乱套?

单例模式:保证一个类仅有一个实例,并提供一个访问它的全局访问点。

巧了,这个模式只有一个类,叫单例类,所以类图我就不画了吧。

单例模式的应用场景举例:牵扯到数据问题,数据库首当其冲,缓存自然也跑不了。

单例类代码实现

//这里是.h文件//老板类单例class Single_Boss{public: static Single_Boss *instence();//获取数据库单例//重点在这个函数 void run();private: Single_Boss(); ~Single_Boss(); char *errmsg;
static Single_Boss *Boss;//实例};




//源文件
Single_Boss *Single_Boss::Boss= NULL;
Single_Boss::Single_Boss(){ cout << "Big Boss" << endl; //debug}
Single_Boss::~Single_Boss(){ cout<<"Seeyou Boss"<<endl;}
Single_Boss* Single_Boss::instence(){ if(!Boss) { Boss= new Single_Boss(); } return Boss;}
void Single_Boss::run(){ cout<<"你好,欢迎光临XXX,请问需要点什么服务?"<<endl;}
int main(){ //Single_Boss *boss = new Single_Boss(); //不信的话大可以将这一行放出来,下面那行屏蔽掉试试 Single_Boss *boss= Single_Boss::instence(); //这是在类外使用单例 boss->run(); return 0;}








提升部分

多线程下的单例模式

曾经有一份真挚的数据库摆在我眼前,可惜我没有去珍惜它,直到我的项目屡屡崩溃,我才知道,如果能重来,我要加个锁。。。

俱往矣,数风流人物,还看今朝。

来我们重新审视一下下面这段代码:


Single_Boss* Single_Boss::instence() //1{  if(!Boss) //2 {  Boss= new Single_Boss(); //3 }  return Boss; }

                                


如果在多线程情况下,一旦有两个线程同时进入了 2 ,怎么办?这不是十分正常的事情吗?一点防范都没有,这不是送人头的行为吗?
白给!!

所以,改一下:


Single_Boss* Single_Boss::instence() //1{  lock(db_mutex); //假设这个锁我已经初始化过了  if(!Boss) //2 {  Boss= new Single_Boss(); //3 }  unlock(db_mutex); //上锁和解锁一定要同时写,就算忘记写中间步骤,也要先写解锁   return Boss; }

 


这样写,可还行?有没有 慧眼识猪 的朋友在下面评论区call个“1”?

这样写的话,每次使用数据库之前都要进行加锁操作,虽然安全了,但是大大地提高了负荷。

所以,再改一下:


Single_Boss* Single_Boss::instence() //1{  if(!Boss){ //一重锁定 lock(db_mutex);  if(!Boss) //二重锁定  {  Boss= new Single_Boss();  }  unlock(db_mutex); //上锁和解锁一定要同时写,就算忘记写中间步骤,也要先写解锁  } return Boss; }




看到这里,可能有的朋友会疑惑:直接把上面那个的 if 判断和锁的位置换一下不就完事儿了吗,为什么要在外面再加上一层,这不是多此一举吗?

还是那个问题:
如果你有两个线程,突破了第一层 if 的防线,及时一个线程会被卡在锁的外面,但是锁仅仅只是锁住了创建单例的部分,当拿到锁的那个线程释放了锁,另一个线程不是照样能拿到锁,创建它的“单例”,那这个锁还有什么意义呢?

而在锁内锁外都加一层 if 判断,当第一个线程进入锁空间,创建完单例,后面的线程即使是拿到了锁,也不会去执行创建单例的步骤。

这,才是一个好的单例模式,这是单例模式中的“懒汉模式”。

饿汉式

有懒汉式,那也有个饿汉式单例。

什么是饿汉式呢?饿汉模式的关键:初始化即实例化

微调上面的代码:

Single_Boss *Single_Boss::Boss= new Single_Boss();
Single_Boss* Single_Boss::instence(){//不需要进行实例化 //if(!Boss) //{ // Boss= new Single_Boss(); //} return Boss;}




一般饿汉式加载所导致的弊端是可能我并不想使用实例但是实例已经被构造,相对于懒汉式的用则构造会造成内存的浪费,但是其实现方式很简单,不用人为加锁保证线程安全。



懒汉还是饿汉?

选哪个可以看个人喜好吧,这里给出一点建议:

懒汉:在访问量较小时,采用懒汉实现。这是以时间换空间。
饿汉:由于要进行线程同步,所以在访问量比较大,或者可能访问的线程比较多时,采用饿汉实现,可以实现更好的性能。这是以空间换时间。


单例模式的优缺点


优点
  - 由于单例模式在内存中只存在一个对象,减少了内存的开支,特别是当对象需要频繁的创建、销毁时,而且创建或销毁时性能又无法优化,单例模式的优势就非常明显。
  - 单例模式可以避免对内存的多重占用。
 - 单例模式可以在系统设置全局的访问点,优化和共享资源访问。这招我经常用,也很喜欢,因为确实方便,做一个标志位单例类,负责所有数据表的映射处理。(要了解可以私信我)

缺点
  - 单例模式一般没有接口,难以拓展。如果要拓展,考虑重构。
 - 单例模式对于测试是不利的。在并发环境中,如果单例没有完成,是不能进行测试的。





还行吧。

创作不易,顺手收藏好习惯,划着划着,就找不到了。