趣文网 > 作文大全

趣说单例模式——选班长

2020-12-16 10:40:01
相关推荐

作者 | 倪升武

责编 | 屠敏

注:本文人物形象均为原创,人物姓名均为虚构。

“码农大学”是“互联省”的一所名牌大学,学习气氛浓厚,不管是学校的环境还是学生综合素质,都非常高。开学的第一天,同学们都兴致勃勃,这不,一起来看下设计模式的课堂里。

自我介绍完之后,老师开始进入本节课的主题了。

提出这个问题后,大家开始相互讨论起来。

懒汉式单例

于是小夏开始实现这个班长类:首先,我们要在班长类中将构造方法私有化,这样是防止在其他地方被实例化,就出现多个班长对象了。然后我们在班长类中自己 new 一个班长对象出来。最后给外界提供一个方法,返回这个班长对象即可。如下(代码可以左右滑动):

publicclassMonitor {privatestatic Monitor monitor = null;privateMonitor() {}publicstatic Monitor getMonitor() {if (monitor == null) { monitor = new Monitor(); }return monitor; }}

小美开始了他的分析:我觉得小夏的代码还是不能保证一个班长实例的,因为存在线程安全问题。假如线程A执行到了monitor = new Monitor();,此时班长对象还没创建,线程B执行到判断 monitor == null时,条件为true,于是也进入到if里面去执行monitor = new Monitor();了,这样内存中就出现了两个班长实例了。

于是,小美根据自己的思路,将小夏的代码做了修改,在获取班长对象的方法上面加了个 synchronized 关键字,这样就能解决线程安全问题了。

publicstaticsynchronized Monitor getMonitor(){if (monitor == null) { monitor = new Monitor(); }return monitor;}

小夏觉得这种修改不太好,于是和小美讨论起来:小美,你这样改虽然可以解决线程安全问题,但是效率太差了,不管班长对象有没有被创建好,后面每个线程并发走到这,可想而知,都做了无用的等待呀。

还没等小美说话,小刘举起手来,他想到了更好的解决方案:老师,我有更好的办法!我们不能在方法上添加 synchronized关键字,但可以在方法内部添加。比如:

publicstatic Monitor getMonitor(){if (monitor == null) {synchronized (Monitor.class) {if (monitor == null) { monitor = new Monitor(); } } }return monitor;}

小刘开始给小夏解释到:这判断是有目的的,第一层判断如果 monitor 实例不为空,那皆大欢喜,说明对象已经被创建过了,直接返回该对象即可,不会走到 synchronized 部分,所以班长对象被创建了之后,不会影响到性能。

第二层判断是在 synchronized 代码块里面,为什么要再做一次判断呢?假如 monitor 对象是 null,那么第一层判断后,肯定有很多线程已经进来第一层了,那么即使在第二层某个线程执行完了之后,释放了锁,其他线程还会进入 synchronized 代码块,如果不判断,那么又会被创建一次,这就导致了多个班长对象的创建。所以第二层起到了一个防范作用。

在同学们踊跃发言和讨论之后,老师做了一下简短的总结:同学们都分析的很棒,这就是“懒汉式”单例模式,为什么称为“懒汉式”呢?顾名思义,就是一开始不创建,等到需要的时候再去创建对象。

小刘的这个“懒汉式”单例模式已经写的很不错了,不过这里还有一个问题,虽然可能已经超出了本课程的要求了,但是我还是来补充一下,在定义班长对象时,要加一个 volatile 关键字。即:

privatestaticvolatile Monitor monitor = null;

于是,老师开始和同学们分析:我们先看下 monitor = new Monitor();,在这个操作中,JVM主要干了三件事:

1、在堆空间里分配一部分空间;

2、执行 Monitor 的构造方法进行初始化;

3、把 monitor 对象指向在堆空间里分配好的空间。

把第3步执行完,这个 monitor 对象就已经不为空了。

但是,当我们编译的时候,编译器在生成汇编代码的时候会对流程顺序进行优化。优化的结果不是我们可以控制的,有可能是按照1、2、3的顺序执行,也有可能按照1、3、2的顺序执行。

如果是按照1、3、2的顺序执行,恰巧在执行到3的时候(还没执行2),突然跑来了一个线程,进来 getMonitor() 方法之后判断 monitor 不为空就返回了 monitor 实例。此时 monitor 实例虽不为空,但它还没执行构造方法进行初始化(即没有执行2),所以该线程如果对那些需要初始化的参数进行操作那就悲剧了。但是加了 volatile 关键字的话,就不会出现这个问题。这是由 volatitle 本身的特性决定的。

关于 volatile 的更多知识已经超出了本课程的范围了,感兴趣的同学可以课后自己研究研究。

饿汉式单例

看到大家一直在激烈的讨论问题,小帅一直在座位上思考……终于他也发言了。

小帅一边说一边写起了代码:

publicclassMonitor {privatestatic Monitor monitor = new Monitor ();privateMonitor(){}publicstatic Monitor getMonitor(){return monitor; }}

小帅继续说到,在定义的时候就将班长对象创建出来,这样还没有线程安全问题。

老师正要讲“饿汉式”单利模式,刚好小帅说出来了,于是就借题发挥:小帅的这种方式就叫做“饿汉式”单例模式,顾名思义,一开始就创建出来,比较“饥饿”,这种方式是不存在线程安全问题的。这个“饿汉式”单利相对来说比较简单,也很好理解,我就不多说了。

单例模式的扩展

听了小帅的发言,小夏开始纳闷了,他开始和旁边的小刘讨论起来,老师好像看出来了小夏有疑惑,于是……

老师借着这个问题,继续讲课:我们要知道,万物存在即合理,但是也不是十全十美的,不管是“懒汉式”还是“饿汉式”,都有它们各自的优缺点以及使用场景。

针对刚刚小夏提到的问题,“饿汉式”虽然简单粗暴,而且线程安全,但是它不是延迟加载的,也就是说类创建的时候,就必须要把这个班长实例创建好,而不是在需要的时候才创建,这是第一点。

我再举个例子,也许更能说明问题:假如在获取班长对象的时候,需要传一个参数进去呢?也就是说,我在选班长的时候有个要求,比如我想选一个身高高于175cm的人做班长,那么我在获取班长实例对象时,需要传一个身高参数,该方法就应该这样设计:

publicstatic Monitor getMonitor(Long height){……}

针对这种情况,“饿汉式”就不行了,就得用“懒汉式”单例了。

静态内部类

老师看了看手表,离下课还有16分钟,于是还想再讲点东西。

于是老师又提出了个问题给同学们:班长这个对象有个属性是不会变的,那就是他所在的班级,所以班级可以直接定义好,老师翻到了PPT的下一页,如:

publicclassMonitor {publicstatic String CLASS_INFO = "通信工程(1)班";privatestatic Monitor monitor = new Monitor ();privateMonitor(){}publicstatic Monitor getMonitor(){return monitor; }}

老师解释到:是可以获取,但是这样获取的话,因为都是static修饰的,调用Monitor.CLASS_INFO时,也会执行构造方法将monitor对象初始化,但是我现在不想初始化班长对象(因为会影响性能),我只想要获取他的班级信息。

于是老师把继续把 PPT 翻到了下一页:

publicclassMonitor {publicstatic String CLASS_INFO = "通信工程(1)班";/** * 静态内部类,用来创建班长对象 */privatestaticclassMonitorCreator {privatestatic Monitor monitor = new Monitor(); }privateMonitor(){}publicstatic Monitor getInstance(){return MonitorCreator.monitor; }}

小美好像发现了新大陆,非常兴奋:我还发现了一个特点,使用静态内部类这种方式,也是实现懒加载的,也就是说当我们调用 getInstance 方法的时候,才会去初始化班长对象,这和“懒汉式”是一样的效果;而且在内部类中,初始化这个班长对象的时候,是直接 new 出来的,这个和“饿汉式”很像。哇,难道这就是两种方式的结合体吗?

枚举单例

老师意犹未尽,但看了看表,还有4分钟就下课了,感觉讲不完了,于是最后给同学们抛出一种方式,让同学们下课后自己研究研究。

于是老师把PPT又往后翻了一页:

publicenum Monitor { INSTANCE;// 其他任意方法}

老师见同学们激情澎湃,于是决定把这个讲完:上面这段枚举代码比较抽象,我说具体点,我们就举前面提到的例子,比如班长有个属性是所属班级,那么我现在要创建这样一个班长实例,我可以这么写:

publicenum Monitor { INSTANCE("通信工程(1)班");private String classInfo; EnumSingleton(String classInfo) {this.classInfo = classInfo; }// 省略get set方法}

于是老师继续往下讲:当你们工作之后,实际场景肯定不像课堂上说的这么简单,就像小刘说的那样,如果有很多属性呢?而且属性可以改变该怎么做呢?这时候,我们可以借助枚举类来实现单例,为什么说“借助”呢?我先创建一个班长对象,里面是属性(这里我就用一个属性代表一下,你们可以认为有很多属性),如下:

publicclassMonitor {private String classInfo;// 省略get set 方法}

接下来,我就要“借助”枚举,创造出班长这个单例实体,而且支持属性可修改,大家请看PPT:

publicenum EnumSingleton { INSTANCE;private Monitor monitor; EnumSingleton() { monitor = new Monitor(); }public Monitor getMonitor(){return monitor; }}

老师对着PPT讲到:Monitor 类就是我们的班长类,我放到私有构造方法中初始化了,然后枚举类中同样提供一个 getMonitor 方法给外界提供这个班长对象,模式和前面讲的单例差不多。我们可以通过 EnumSingleton.INSTANCE.getMonitor(); 即可获取到 monitor 对象。

就这样,老师被几个学生架到生活区的小饭馆了,当然咯,最后还少不了买单……

作者简介:倪升武,CSDN 博客专家,CSDN达人课作者。硕士毕业于同济大学,曾先后就职于 eBay、爱奇艺、华为。目前在科大讯飞从事Java领域的软件开发,他的世界不仅只有coding。声明:本文为作者投稿,版权归其个人所有。

阅读剩余内容
网友评论
相关内容
延伸阅读
小编推荐

大家都在看

关于电的作文 英语作文我的大学 农村生活英语作文 别人帮我的作文400字 观后感英语作文 爱使我坚强作文 关于桥的作文800字 作文名 关于清明节由来的作文 采访作文100字 读你作文800字 写景作文400字以上 我依然相信作文600字 回忆学校的作文 古诗改写作文200字 金边凤尾裙作文 最美的相遇作文600字 感谢师恩作文 2013湖北高考作文题 小猫作文说明文 森林运动会童话作文 雷锋的作文400字 四年级作文批改评语 感谢国家资助作文800字 知易行难的作文 糖炒栗子的作文 妈妈教我包饺子作文 为人生着色作文 校园的回忆作文 热心的邻居作文