戏说设计模式 - 只有一个中国 - 单例模式

Posted java新人交流

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了戏说设计模式 - 只有一个中国 - 单例模式相关的知识,希望对你有一定的参考价值。

戏说设计模式 - 单例模式

[toc]

简介

Singleton
保证类有且仅有一个实例,并提供一个访问它的全局站点

  • 类型

    • 饿汉模式

    • 懒汉模式

                            



懒汉模式 PK 饿汉模式

/ 懒汉模式 饿汉模式 备注
线程安全 不安全 安全
类加载速度 饿汉模式在类加载时就初始化其唯一的实例而懒汉模式不需要,所以饿汉模式在类加载时速度会慢一点
运行速度 饿汉模式在类加载时就已经完成了其唯一实例的初始化。而懒汉模式还需要进行初始化操作,所以懒汉模式在运行时速度会慢一点


优缺点

  • 优点

    • 由于单例模式在内存中只有一个实例,减少了内存开支,特别是一个对象需要频繁地创建、销毁时,而且创建或销毁时性能又无法优化,单例模式的优势就非常明显。

    • 由于单例模式只生成一个实例,所以减少了系统的性能开销,当一个对象的产生需要比较多的资源时,如读取配置、产生其他依赖对象时,则可以通过在应用启动时直接产生一个单例对象,然后用永久驻留内存的方式来解决(在Java EE中采用单例模式时需要注意JVM垃圾回收机制)。

    • 单例模式可以避免对资源的多重占用,例如一个写文件动作,由于只有一个实例存在内存中,避免对同一个资源文件的同时写操作。

    • 单例模式可以在系统设置全局的访问点,优化和共享资源访问,例如可以设计一个单例类,负责所有数据表的映射处理。

  • 缺点

    • 单例模式一般没有接口,扩展很困难,若要扩展,除了修改代码基本上没有第二种途径可以实现。单例模式为什么不能增加接口呢?因为接口对单例模式是没有任何意义的,它要求“自行实例化”,并且提供单一实例、接口或抽象类是不可能被实例化的。当然,在特殊情况下,单例模式可以实现接口、被继承等,需要在系统开发中根据环境判断。

    • 单例模式对测试是不利的。在并行开发环境中,如果单例模式没有完成,是不能进行测试的,没有接口也不能使用mock的方式虚拟一个对象。

    • 单例模式与单一职责原则有冲突。一个类应该只实现一个逻辑,而不关心它是否是单例的,是不是要单例取决于环境,单例模式把“要单例”和业务逻辑融合在一个类中。


适用场景

  • 应用程序中的该对象有且只有一个即可

  • 创建一个对象需要消耗的资源过多,如要访问IO和数据库等资源;

  • 需要定义大量的静态常量和静态方法(如工具类)的环境,可以采用单例模式(当然,也可以直接声明为static的方式)。

注意事项

  • 在高并发情况下,请注意单例模式的线程同步问题。

案例

只有一个中国

小鸟:“该死的日本鬼子,...”

大牛:“小鸟,你突然骂日本人干啥?”

小鸟:“连一个怀孕六个月的孕妇都不放过,居然将刺刀插进孕妇的腹部,挑出一个还没出世的孩子,好好的一个南京城,硬是被他们弄的血流成河.”

大牛:“你在看《南京大屠杀》吧!在那个贫穷积弱的年代,又何止是一个南京,整个华夏大地都被他们弄的乌烟瘴气,幸好,咱们的前辈们站起来了,打赢了那一场硬仗。”

小鸟:“是啊!胜利来之不易,所以我们要坚持只有一个中国的原则。到现在了,居然还有人闹藏独,台独,真想化身成欧特曼,钢铁侠,谁敢不服,谁敢闹分裂,就揍谁!”

大牛:“哈哈,想不到我们小鸟还是一个这么有血性的人,不过大象是不会去计较蚂蚁的挑衅的。”

大牛:“香港回归,你能用程序实现吗?”

小鸟:“我编写一个中国类,定义一个城市返回的方法,这里定义为带参的(参数:城市),因为我们都知道,香港回归后,有澳门回归,我更期待不久的将来,由台湾回归.这是事实的角度出发,同时也是从系统的扩展性出发”

public class China {
    public void returnChina(String city) {
        System.out.println(city+"回归");
    }
}

小鸟:“客户端调用,香港回归”

China china = new China();
china.returnChina("香港");

大牛:“小鸟,说的不错,扩展性你已经考虑到了,但是就像你前面说的胜利来之不易,我们要坚持只有一个中国的原则,而你这里有体现没?”

小鸟:“没有,我的中国类是new出来的,如果我不new的话,我就没法创建对象了啊”

大牛:“你可以将它的构造方法私有化啊。”

懒汉模式实现

私有化构造方法:限制产生多个对象

小鸟:“构造方法私有化,是将构造方法的修饰符改成private吗?”

private China() {
}

大牛:“是的。”

创建类的唯一实例:创建中国类的唯一实例,并使用private static 修饰

小鸟:“这样改了,我外部就没法实例化了哦,不要说台湾回归,就是已经回归了的香港,澳门,都回归不了啊?”

大牛:“private的作用域是?”

小鸟:“在其类的内部是可以使用的,我明白了,你是说让我在中国类里面声明一个自己的对象,然后通过get方法让它自行实例化.同时也是为了提供给外部使用”

大牛:“不错不错,一点就通,不过能将代码写出来,才表示真正理解了哦!”

小鸟:“这还不容易”
单例对象:China

类中其它方法尽量是static

public class China {
    // 自行实例化
    private static China china;
    private China() {
    }
    // 返回我自己的对象
    public static China getChina() {
        if (china == null) {
            china = new China();
        }
        return china;
    }
    public void returnChina(String city) {
        System.out.println(city + "回归");
    }
}

客户端

China china = China.getChina();
china.returnChina("香港");

大牛:“不错,这里还有一点需要知道的就是,类中的其它方法,尽量是static

小鸟:“ok,我马上把returnChina改成static修饰.”

饿汉模式实现

大牛:“这里还有一个问题需要说明一下,就是关于并发,会导致线程不安全。”

小鸟:“并发?”

大牛:“换句话说吧!假设我们的香港和澳门是同一天同一时刻回归的,那么也就代表会同时去访问China类 ---> 由于开始并没有任何城市访问过China类,所以会导致它们都进入getChina()方法中的if语句里面 ---> 这样的话就会造成我们的china会new两次。”

小鸟:“我明白了,我们这个单例模式,就是为了保证整个应用程序中有且只有一个该对象,像开始那种低并发的情况,我那样写是没什么问题的,但是如果要考虑高并发的话,就不能这么写了,如果不这么写,我又该如何写呢?”

大牛:“不错我们的小鸟还挺善于总结的,关于并发导致线程不安全,我们这里有几种解决方案。”

第1种:synchronized

在getChina方法前加synchronize关键字
synchronize作用:用于解决线程同步的问题, eg:A,B同时访问方法c, 如果不加synchronize,则同时访问,如果加了B会等A访问完在进行访问

大牛:“第1种,就是在getChina方法前加synchronized,它的作用就是香港,澳门同一时刻回归时,它们种任意一个获取完中国对象,另一个才会去获取对象,也就解决了会同时产生两个中国对象的问题.”

public static synchronized China getChina() {
    if (china == null) {
        china = new China();
    }
    return china;
}

第2种:双重锁定

Double-Check Locking

小鸟:“如果是这样的话,确实是能解决同时产生两个中国对象的问题,但是每次获取China对象时,都会进行锁定,这样不好吧!会影响性能吧?”

大牛:“确实,分析的不错,所以这里我就给你讲一下第2种实现方式:双重锁定,具体代码如下:”

public static China getChina() {
    if (china == null) {
        synchronized (new Object()) {
            if (china == null) {
                china = new China();
            }
        }
    }
    return china;
}

大牛:“我们这里在加锁前面进行了一次判断,所以就解决了我们每次都要进行加锁操作的问题,我们再加锁的代码块中再次进行判断,就解决了我们会产生两个对象的问题.”

第3种:饿汉模式
小鸟:“如果是这样的话,确实是能解决同时产生两个中国对象的问题,但是我们假设当前第1个回归的是香港,香港正在获取中国这个对象,但出了意外,大不列颠及北爱尔兰联合王国政府欠揍,突然反悔,那么我们的香港就会一直处于查找中国的阶段,由于我们加了synchronized关键字,它会解决并发的问题,对线程进行一个排序,从而也就导致我们的香港如果不回归的话,我们的澳门也就不能回归了,这样是不是太亏了啊!”

大牛:“分析的不错,确实会有这个问题,所以我们这里推荐第3种实现方式,饿汉模式,即在类进行装载的时候,就对其进行实例化.”

public class China {
    // 自行实例化
    private static final China china = new China();
    private China() {
    }
    // 返回我自己的对象
    public static China getChina() {
        return china;
    }
    // 类中其它方法,建议用static修饰
    public static void returnChina(String city) {
        System.out.println(city + "回归");
    }
}

小鸟:“这样就完美了!”

大牛:“非也,非也,我们这3种方式都是从线程安全的角度来考虑,第1种:线程安全,但是严重影响性能;第2种:线程安全,性能优于第1种;第3种:线程安全,性能优于前面两种,但是由于类装载的时候,就进行了唯一实例的初始化,会导致内存的浪费。”

小鸟:“那我采用哪种比较优呢?”

大牛:“我们一般是采用第3种,内存浪费点就浪费点,现在设备都不差这点内存,最主要的是运行时,由于一开始已经加载,所以运行速度会快一点。”


以上是关于戏说设计模式 - 只有一个中国 - 单例模式的主要内容,如果未能解决你的问题,请参考以下文章

单例模式

单例模式

Java设计模式-单例设计模式

戏说设计

那么静,那么美,单例模式

单例模式以及静态代码块