前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >趣说单例模式——选班长

趣说单例模式——选班长

作者头像
Java团长
发布2019-04-25 15:30:43
3930
发布2019-04-25 15:30:43
举报

来源:程序员私房菜(ID:eson_15)

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

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

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

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

1. 懒汉式单例

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

代码语言:javascript
复制
public class Monitor {
   private static Monitor monitor = null;
   private Monitor() {}
   public static Monitor getMonitor() {
       if (monitor == null) {
           monitor = new Monitor();
       }
       return monitor;
   }
}

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

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

代码语言:javascript
复制
public static synchronized Monitor getMonitor() {
   if (monitor == null) {
       monitor = new Monitor();
   }
   return monitor;
}

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

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

代码语言:javascript
复制
public static Monitor getMonitor() {
   if (monitor == null) {
       synchronized (Monitor.class) {
           if (monitor == null) {
               monitor = new Monitor();
           }
       }
   }
   return monitor;
}

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

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

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

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

代码语言:javascript
复制
private static volatile 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 的更多知识已经超出了本课程的范围了,感兴趣的同学可以课后自己研究研究。

2. 饿汉式单例

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

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

代码语言:javascript
复制
public class Monitor {
   private static Monitor monitor = new Monitor ();
   private  Monitor () {}
   public static Monitor getMonitor() {
       return monitor;
   }
}

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

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

3. 单例模式的扩展

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

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

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

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

代码语言:javascript
复制
public static Monitor getMonitor(Long height) {……}

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

3.1 静态内部类

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

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

代码语言:javascript
复制
public class Monitor {
   public static String CLASS_INFO = "通信工程(1)班";
   private static Monitor monitor = new Monitor ();
   private Monitor () {}
   public static Monitor getMonitor() {
       return monitor;
   }
}

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

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

代码语言:javascript
复制
public class Monitor {
   public static String CLASS_INFO = "通信工程(1)班";
   /**
    * 静态内部类,用来创建班长对象
    */
   private static class MonitorCreator {
       private static Monitor monitor = new Monitor();
   }
   private Monitor() {}
   public static Monitor getInstance() {
       return MonitorCreator.monitor;
   }
}

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

3.2 枚举单例

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

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

代码语言:javascript
复制
public enum Monitor {
   INSTANCE;
   // 其他任意方法
}

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

代码语言:javascript
复制
public enum Monitor {
   INSTANCE("通信工程(1)班");
   private String classInfo;
   EnumSingleton(String classInfo) {
       this.classInfo = classInfo;
   }
   // 省略get set方法
}

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

代码语言:javascript
复制
public class Monitor {
   private String classInfo;
   private Monitor() {}
   //省略get set方法
}

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

代码语言:javascript
复制
public enum EnumSingleton {
   INSTANCE;
   private Monitor monitor;
   EnumSingleton() {
       monitor = new Monitor();
   }
   public Monitor getMonitor() {
       return monitor;
   }
}

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

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

(完)

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-01-26,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Java团长 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档