漫画:什么是单例设计模式?

举报
feichaiyu 发表于 2019/11/06 17:43:15 2019/11/06
【摘要】 为什么这样写呢?我们来解释几个关键点:1.要想让一个类只能构建一个对象,自然不能让它随便去做new操作,因此Signleton的构造方法是私有的。2.instance是Singleton类的静态成员,也是我们的单例对象。它的初始值可以写成Null,也可以写成new Singleton()。至于其中的区别后来会做解释。3.getInstance是获取单例对象的方法。如果单例初始值是null,还...

1573032787278579.png

1573032787162615.png

1573032787215519.png

1573032788817215.png

1573032788115114.png

为什么这样写呢?我们来解释几个关键点:


1.要想让一个类只能构建一个对象,自然不能让它随便去做new操作,因此Signleton的构造方法是私有的。


2.instance是Singleton类的静态成员,也是我们的单例对象。它的初始值可以写成Null,也可以写成new Singleton()。至于其中的区别后来会做解释。


3.getInstance是获取单例对象的方法。


如果单例初始值是null,还未构建,则构建单例对象并返回。这个写法属于单例模式当中的懒汉模式。


如果单例对象一开始就被new Singleton()主动构建,则不再需要判空操作,这种写法属于饿汉模式


这两个名字很形象:饿汉主动找食物吃,懒汉躺在地上等着人喂。

1573032830876321.png

为什么说刚才的代码不是线程安全呢?


假设Singleton类刚刚被初始化,instance对象还是空,这时候两个线程同时访问getInstance方法:

1573032867285556.png

这样一来,显然instance被构建了两次。让我们对代码做一下修改:

1573032892549551.png

为什么这样写呢?我们来解释几个关键点:


1.为了防止new Singleton被执行多次,因此在new操作之前加上Synchronized 同步锁,锁住整个类(注意,这里不能使用对象锁)。


2.进入Synchronized 临界区以后,还要再做一次判空。因为当两个线程同时访问的时候,线程A构建完对象,线程B也已经通过了最初的判空验证,不做第二次判空的话,线程B还是会再次构建instance对象。

1573032975282188.png

1573032976796863.png

1573032976831092.png

1573032976915911.png

1573032977364488.png

1573033020970749.png

1573033020244786.png

假设这样的场景,当两个线程一先一后访问getInstance方法的时候,当A线程正在构建对象,B线程刚刚进入方法:

1573033070513698.png

这种情况表面看似没什么问题,要么Instance还没被线程A构建,线程B执行 if(instance == null)的时候得到false;要么Instance已经被线程A构建完成,线程B执行 if(instance == null)的时候得到true。


真的如此吗?答案是否定的。这里涉及到了JVM编译器的指令重排


指令重排是什么意思呢?比如java中简单的一句 instance = new Singleton,会被编译器编译成如下JVM指令:


memory =allocate();    //1:分配对象的内存空间 

ctorInstance(memory);  //2:初始化对象 

instance =memory;     //3:设置instance指向刚分配的内存地址 


但是这些指令顺序并非一成不变,有可能会经过JVM和CPU的优化,指令重排成下面的顺序:


memory =allocate();    //1:分配对象的内存空间 

instance =memory;     //3:设置instance指向刚分配的内存地址 

ctorInstance(memory);  //2:初始化对象 


当线程A执行完1,3,时,instance对象还未完成初始化,但已经不再指向null。此时如果线程B抢占到CPU资源,执行  if(instance == null)的结果会是false,从而返回一个没有初始化完成的instance对象。如下图所示:

1573033157451902.png

1573033158829899.png

如何避免这一情况呢?我们需要在instance对象前面增加一个修饰符volatile。

1573033252623294.png

1573033253772280.png

1573033253735046.png

经过volatile的修饰,当线程A执行instance = new Singleton的时候,JVM执行顺序是什么样?始终保证是下面的顺序:


memory =allocate();    //1:分配对象的内存空间 

ctorInstance(memory);  //2:初始化对象 

instance =memory;     //3:设置instance指向刚分配的内存地址 


如此在线程B看来,instance对象的引用要么指向null,要么指向一个初始化完毕的Instance,而不会出现某个中间态,保证了安全。

1573033299134195.png

1573033299303529.png

几点说明:


1. volatile关键字不但可以防止指令重排,也可以保证线程访问的变量值是主内存中的最新值。有关volatile的详细原理,我在以后的漫画中会专门讲解。


2.本漫画纯属娱乐,还请大家尽量珍惜当下的工作,切勿模仿小灰的行为哦。




—————END—————




喜欢本文的朋友们,欢迎长按下图关注订阅号程序员小灰,收看更多精彩内容

1573033320886119.jpg

转载声明:本文转载自公众号【程序员小灰】

原文链接:https://mp.weixin.qq.com/s/-Iww0kShHBoo8dPHDRjAXA

【版权声明】本文为华为云社区用户转载文章,如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。