码农课堂:趣说单例模式——选班长

640?wx_fmt=png

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

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

640?wx_fmt=jpeg

640?wx_fmt=jpeg

640?wx_fmt=jpeg

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

640?wx_fmt=jpeg

640?wx_fmt=jpeg

640?wx_fmt=jpeg

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

640?wx_fmt=jpeg

640?wx_fmt=jpeg

640?wx_fmt=jpeg

640?wx_fmt=jpeg

640?wx_fmt=jpeg

1. 懒汉式单例

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

public class Monitor {
    private static Monitor monitor = null;
    private Monitor() {}
    public static Monitor getMonitor() {
        if (monitor == null) {
            monitor = new Monitor();
        }
        return monitor;
    }
}

640?wx_fmt=jpeg

640?wx_fmt=jpeg

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

640?wx_fmt=jpeg

640?wx_fmt=jpeg

640?wx_fmt=jpeg

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

public static synchronized Monitor getMonitor() {
    if (monitor == null) {
        monitor = new Monitor();
    }
    return monitor;
}

640?wx_fmt=jpeg

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

640?wx_fmt=jpeg

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

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

640?wx_fmt=jpeg

640?wx_fmt=jpeg

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

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

640?wx_fmt=jpeg

640?wx_fmt=jpeg

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

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

private static volatile Monitor monitor = null;

640?wx_fmt=jpeg

640?wx_fmt=jpeg

于是,老师开始和同学们分析:我们先看下 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. 饿汉式单例

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

640?wx_fmt=jpeg

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

public class Monitor {
    private static Monitor monitor = new Monitor ();
    private  Monitor () {}
    public static Monitor getMonitor() {
        return monitor;
    }
}

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

640?wx_fmt=jpeg

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

3. 单例模式的扩展

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

640?wx_fmt=jpeg

640?wx_fmt=jpeg

640?wx_fmt=jpeg

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

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

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

public static Monitor getMonitor(Long height) {……}

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

640?wx_fmt=jpeg

640?wx_fmt=jpeg

3.1 静态内部类

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

640?wx_fmt=jpeg

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

public class Monitor {
    public static String CLASS_INFO = "通信工程(1)班";
    private static Monitor monitor = new Monitor ();
    private Monitor () {}
    public static Monitor getMonitor() {
        return monitor;
    }
}

640?wx_fmt=jpeg

640?wx_fmt=jpeg

640?wx_fmt=jpeg

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

640?wx_fmt=jpeg

640?wx_fmt=jpeg

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

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;
    }
}

640?wx_fmt=jpeg

640?wx_fmt=jpeg

640?wx_fmt=jpeg

640?wx_fmt=jpeg

640?wx_fmt=jpeg

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

640?wx_fmt=jpeg

640?wx_fmt=jpeg

640?wx_fmt=jpeg

640?wx_fmt=jpeg

640?wx_fmt=jpeg

640?wx_fmt=jpeg

3.2 枚举单例

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

640?wx_fmt=jpeg

640?wx_fmt=jpeg

640?wx_fmt=jpeg

640?wx_fmt=jpeg

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

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

640?wx_fmt=jpeg

640?wx_fmt=jpeg

640?wx_fmt=jpeg

640?wx_fmt=jpeg

640?wx_fmt=jpeg

640?wx_fmt=jpeg

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

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

640?wx_fmt=jpeg

640?wx_fmt=jpeg

640?wx_fmt=jpeg

640?wx_fmt=jpeg

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

public class Monitor {
    private String classInfo;
    // 省略get set 方法
}

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

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

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

640?wx_fmt=jpeg

640?wx_fmt=jpeg

640?wx_fmt=jpeg

640?wx_fmt=jpeg

640?wx_fmt=jpeg

640?wx_fmt=jpeg

640?wx_fmt=jpeg

640?wx_fmt=jpeg

640?wx_fmt=jpeg

640?wx_fmt=jpeg

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

END

码农课堂是【程序员私房菜】推出的一个新的版块。一位老师,四位同学,将技术把玩于课堂之上。旨在用诙谐幽默、通俗易懂的语言给读者分享技术细节。

往期精彩:

关注我

每天进步一点点

640?wx_fmt=png

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值