“码农大学”是“互联省”的一所名牌大学,学习气氛浓厚,不管是学校的环境还是学生综合素质,都非常高。开学的第一天,同学们都兴致勃勃,这不,一起来看下设计模式的课堂里。 自我介绍完之后,老师开始进入本节课的主题了。 提出这个问题后,大家开始相互讨论起来。 懒汉式单例 于是小夏开始实现这个班长类:首先,我们要在班长类中将构造方法私有化,这样是防止在其他地方被实例化,就出现多个班长对象了。然后我们在班长类中自己 new 一个班长对象出来。最后给外界提供一个方法,返回这个班长对象即可。如下(代码可以左右滑动): public class Monitor { 小美开始了他的分析:我觉得小夏的代码还是不能保证一个班长实例的,因为存在线程安全问题。假如线程A执行到了monitor = new Monitor();,此时班长对象还没创建,线程B执行到判断 monitor == null时,条件为true,于是也进入到if里面去执行monitor = new Monitor();了,这样内存中就出现了两个班长实例了。 于是,小美根据自己的思路,将小夏的代码做了修改,在获取班长对象的方法上面加了个 synchronized 关键字,这样就能解决线程安全问题了。
小夏觉得这种修改不太好,于是和小美讨论起来:小美,你这样改虽然可以解决线程安全问题,但是效率太差了,不管班长对象有没有被创建好,后面每个线程并发走到这,可想而知,都做了无用的等待呀。 还没等小美说话,小刘举起手来,他想到了更好的解决方案:老师,我有更好的办法!我们不能在方法上添加 synchronized关键字,但可以在方法内部添加。比如: public static Monitor getMonitor() { 小刘开始给小夏解释到:这判断是有目的的,第一层判断如果 monitor 实例不为空,那皆大欢喜,说明对象已经被创建过了,直接返回该对象即可,不会走到 synchronized 部分,所以班长对象被创建了之后,不会影响到性能。 第二层判断是在 synchronized 代码块里面,为什么要再做一次判断呢?假如 monitor 对象是 null,那么第一层判断后,肯定有很多线程已经进来第一层了,那么即使在第二层某个线程执行完了之后,释放了锁,其他线程还会进入 synchronized 代码块,如果不判断,那么又会被创建一次,这就导致了多个班长对象的创建。所以第二层起到了一个防范作用。 在同学们踊跃发言和讨论之后,老师做了一下简短的总结:同学们都分析的很棒,这就是“懒汉式”单例模式,为什么称为“懒汉式”呢?顾名思义,就是一开始不创建,等到需要的时候再去创建对象。 小刘的这个“懒汉式”单例模式已经写的很不错了,不过这里还有一个问题,虽然可能已经超出了本课程的要求了,但是我还是来补充一下,在定义班长对象时,要加一个 volatile 关键字。即:
于是,老师开始和同学们分析:我们先看下 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 的更多知识已经超出了本课程的范围了,感兴趣的同学可以课后自己研究研究。 饿汉式单例 看到大家一直在激烈的讨论问题,小帅一直在座位上思考……终于他也发言了。 小帅一边说一边写起了代码: public class Monitor { 小帅继续说到,在定义的时候就将班长对象创建出来,这样还没有线程安全问题。 老师正要讲“饿汉式”单利模式,刚好小帅说出来了,于是就借题发挥:小帅的这种方式就叫做“饿汉式”单例模式,顾名思义,一开始就创建出来,比较“饥饿”,这种方式是不存在线程安全问题的。这个“饿汉式”单利相对来说比较简单,也很好理解,我就不多说了。 单例模式的扩展 听了小帅的发言,小夏开始纳闷了,他开始和旁边的小刘讨论起来,老师好像看出来了小夏有疑惑,于是…… 老师借着这个问题,继续讲课:我们要知道,万物存在即合理,但是也不是十全十美的,不管是“懒汉式”还是“饿汉式”,都有它们各自的优缺点以及使用场景。 针对刚刚小夏提到的问题,“饿汉式”虽然简单粗暴,而且线程安全,但是它不是延迟加载的,也就是说类创建的时候,就必须要把这个班长实例创建好,而不是在需要的时候才创建,这是第一点。 我再举个例子,也许更能说明问题:假如在获取班长对象的时候,需要传一个参数进去呢?也就是说,我在选班长的时候有个要求,比如我想选一个身高高于175cm的人做班长,那么我在获取班长实例对象时,需要传一个身高参数,该方法就应该这样设计:
针对这种情况,“饿汉式”就不行了,就得用“懒汉式”单例了。 静态内部类 老师看了看手表,离下课还有16分钟,于是还想再讲点东西。 于是老师又提出了个问题给同学们:班长这个对象有个属性是不会变的,那就是他所在的班级,所以班级可以直接定义好,老师翻到了PPT的下一页,如: public class Monitor { 老师解释到:是可以获取,但是这样获取的话,因为都是static修饰的,调用Monitor.CLASS_INFO时,也会执行构造方法将monitor对象初始化,但是我现在不想初始化班长对象(因为会影响性能),我只想要获取他的班级信息。 于是老师把继续把 PPT 翻到了下一页:
小美好像发现了新大陆,非常兴奋:我还发现了一个特点,使用静态内部类这种方式,也是实现懒加载的,也就是说当我们调用 getInstance 方法的时候,才会去初始化班长对象,这和“懒汉式”是一样的效果;而且在内部类中,初始化这个班长对象的时候,是直接 new 出来的,这个和“饿汉式”很像。哇,难道这就是两种方式的结合体吗? 枚举单例 老师意犹未尽,但看了看表,还有4分钟就下课了,感觉讲不完了,于是最后给同学们抛出一种方式,让同学们下课后自己研究研究。 于是老师把PPT又往后翻了一页: public enum Monitor { 老师见同学们激情澎湃,于是决定把这个讲完:上面这段枚举代码比较抽象,我说具体点,我们就举前面提到的例子,比如班长有个属性是所属班级,那么我现在要创建这样一个班长实例,我可以这么写:
于是老师继续往下讲:当你们工作之后,实际场景肯定不像课堂上说的这么简单,就像小刘说的那样,如果有很多属性呢?而且属性可以改变该怎么做呢?这时候,我们可以借助枚举类来实现单例,为什么说“借助”呢?我先创建一个班长对象,里面是属性(这里我就用一个属性代表一下,你们可以认为有很多属性),如下: public class Monitor { 接下来,我就要“借助”枚举,创造出班长这个单例实体,而且支持属性可修改,大家请看PPT:
老师对着PPT讲到:Monitor 类就是我们的班长类,我放到私有构造方法中初始化了,然后枚举类中同样提供一个 getMonitor 方法给外界提供这个班长对象,模式和前面讲的单例差不多。我们可以通过 EnumSingleton.INSTANCE.getMonitor(); 即可获取到 monitor 对象。 就这样,老师被几个学生架到生活区的小饭馆了,当然咯,最后还少不了买单…… 作者简介:倪升武,CSDN 博客专家,CSDN达人课作者。硕士毕业于同济大学,曾先后就职于 eBay、爱奇艺、华为。目前在科大讯飞从事Java领域的软件开发,他的世界不仅只有coding。 |
|