Android设计模式之——单例模式

一.什么是单例模式

单例模式的定义:确保一个类只有一个实例,并提供一个访问他的全局访问点。单例模式是几个设计模式中最简单也是应用最广泛的模式。在应用这个模式时,单例对象的类必须保证只有一个实例存在。避免产生多个对象消耗过多的资源,或者某种类型的对象只应该有且只有一个。例如,创建一个对象需要消耗的资源过多,如要访问IO和数据库等资源,这时就要考虑使用单例模式。
  这里举两个我们在Android应用开发中经常见到的例子来说明:
①全局对象Application的使用
  Application和Activity,Service一样是Android框架的一个系统组件,当Android程序启动时系统会创建一个Application对象且只创建一个,用来存储系统的一些信息,所以Application是单例(singleton)模式的一个类
  通场我们在开发Android应用的时候都会指定我们自己的Appliction类——创建一个类继承Application并在AndroidManifest.xml文件中的application标签中进行注册(只需要给application标签增加name属性,并添加自己的 Application的名字即可)。此时我们只要在我们自定义的Application类中定义getInstance()方法并返回Application实例就可以了:

public class RaisingPetsApplication extends Application {
    private static RaisingPetsApplication instance;
    @Override
    public void onCreate() {
        super.onCreate();
        instance = this;
    }
    public static RaisingPetsApplication getInstance(){
        return instance;
    }
}

这个时候我们就可以在整个APP中调用RaisingPetsApplication的实例来进行各种操作了:

jobManager = RaisingPetsApplication.getInstance().getJobManager();
RaisingPetsApplication.getInstance().addActivity(this);
......

可能懂单例模式写法的读者会比较疑惑,这上面那段代码中的写法并不属于任何一种单例模式啊?事实上Application类的单例模式是在Android程序创建的时候通过系统内部源码创建的(都说了是系统级别的组件),上面的写法不过展示了如何调用这个实例,并没有创建什么实例。关于Application如何创建单例,后面我们会通过系统源码来分析。
②Retrofit实例创建
  Retrofit类大家都不陌生(现在做应用都要这玩意吧?)同样在应用中我们可能要多次用到Retrofit的实例,这个场景就可以用到单例模式。现在假设我们的 应用中要用到好多个BASE_URL,这个时候就出现了下面的写法:

public enum AppNetWork {
    INSTANCE;   //instance

    private NetWorkApi mApi;
    AppNetWork() {
        Retrofit retrofit = new Retrofit.Builder()
                .baseUrl("BASE_URL_1")
                .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
                .addConverterFactory(GsonConverterFactory.create())
                .build();
        mApi = retrofit.create(NetWorkApi.class);
        ......
    }

    public NetWorkApi getApi() {
        return mApi;
    }
    ......
}

之后我们再建三个接口类(这里建了一个演示一下)用来存放三个BASE_URL对应的子url:

public interface NetWorkApi {
    @GET("url")
    Observable<Bean> getData();
    ......
}

之后就可以通过下面这句调用接口了:

AppNetWork.INSTANCE.getApi().getData()

需要说明的是,这里用的是枚举单例方法,你当然也可以通过其他单例方法来创建。
  基于单例模式特点,单例对象通常作为程序中存放配置信息的载体(就像上面的Application中我们经常在里面做一些配置的初始化),因为它能够保证其他对象读取到一致的信息

二.如何实现单例模式

1.从new一个对象说起

假设我们现在有一个Singleton类,他有一个公有的构造方法和一个共公有的字元素child:

public class Singleton {
     public String child ;
     public Singleton(){
     }
 }

这个时候我们要在别的类中对他进行操作。最一般的做法我们直接就在另一个类中执行:

public class Operation{
    private Singleton mSingleton;
    public Operation(){
        mSingleton = new Singleton();
        init();
    }
    private void init(){
        mSingleton.child = "测试";
    }
}

这样就完成了一个对Singleton类的赋值操作。由于Singleton的构造方法是公有的,任何别的类都可以创建他的实例并对其中的共有属性进行操作,当然无法满足确保一个类只有一个实例,并提供一个访问他的全局访问点的要求了。
  既然大家都可以通过Singleton的公有构造方法来创建他的实例,那么我们把他的构造方法改成private!就行了。但是这样的话,别的类就直接无法通过new获取Singleton类的实例,一次创建实例的机会都没有了,也无法达到要求。这个时候我们就想了,别的类无法在外面new一个Singleton对象,但是在Singleton类的内部应该还是可以自己创建自己的实例吧?当然可以:

 public class Singleton {
    private Singleton single = new Singleton();
     private Singleton() {
     }
 }

这样一来,我们虽然在内部创建了一个本类的实例,但是由于该类的构造方法已经被封死了别的类进不来,怎么获得这个实例呢?注意定义的后半句——"提供一个访问他的全局访问点"!也就是说,我们需要一个访问点,来将这个Singleton内部创建的实例暴露出去。这个时候我们就想了,既然别的类不能new他的实例,还想在别的类中调用他的元素,那我们可以创建静态元素,这样直接就可以通过类名来调用了,绕开了new这个坎。最终我们在Singleton类中创建一个public的静态方法getInstance(),通过该方法来返回Singleton类的实例:

public class Singleton {    //饿汉模式,在类初始化时自行实例化
     private static final Singleton single = new Singleton();

     private Singleton() {
     }
     //静态工厂方法
     public static Singleton getInstance() {
         return single;
     }
 }

上面这个程序就是我们创建的一个最简单的饿汉式单例。饿汉式是指在创建对象实例的时候就比较着急,饿嘛,于是在装载类的时候就创建对象实例private static final Singleton single = new Singleton();上面例子中,在这个类被加载时,静态变量single会被初始化,此时类的私有构造函数会被调用。这时单例类的唯一实例就被构造出来了
  我们总结一下上面说的,对于一个单例类来说,需要满足一下几个条件:
①构造方法为private,堵死了外界利用new创建此类实例的可能。
②内部有一个公有的静态方法,用于返回该类内部创建的实例。(枚举类除外)
③确保单例类的对象有且仅有一个,尤其是在多线程环境下
④确保按单例对象在反序列化时不会重新构建对象。
  上面这这几点最大的好处就是:因为我们要控制Singleton类实例的数量,在别的类中当然不好控制,但是现在在我自己内部创建,自己内部返回。这样一来我就比较好控制我的实例了(比如在多线程环境中确保构造的对象仍然是有且仅有一个,并保证线程安全),毕竟一切都在我自己内部。

2.懒汉模式

上述饿汉模式虽然在大多数情况下保证了单例,但是最大的缺点就是当类装载时直接创建类的实例,不论是否会用到这个实例。这个时候我们需要考虑到多线程并发的情况。由于一加载这个类他就会创建一个实例,假如我们现在有多个线程同时访问这个类,此时还是会创建多个类的实例出来。为了解决这个问题,我们对上述饿汉模式做一下改进,就得到了懒汉模式:

public class Singleton {    //懒汉式单例,在第一次调用getInstance()时实例化自己
    private Singleton() {}
    private static Singleton single=null;
    //静态工厂方法
    public static synchronized Singleton getInstance() {
         if (single == null) {
             single = new Singleton();
         }
        return single;
    }
}

懒汉式意味着,既然懒,那么在创建对象实例的时候就不着急。一直等到调用返回该实例的方法getInstance()的时候才会被创建,懒人嘛,总是推脱不开的时候才会真正执行工作,因此在装载对象的时候不创建对象实例
  上面我们用到了synchronized关键字来锁住getInstance()。也就是说getInstance()方法块只能运行在一个线程中,如果该段代码已经在一个线程中运行,另外一个线程试图运行这块代码,他会被阻塞而一直等待。而在这个线程安全的方法块中我们进行了Singleton的实例化:single = new Singleton();这样一来就保证了多线程模式下单例对象的唯一性。
  懒汉单例模式得优点是单例模式只有在使用时才会被实例化,相比饿汉模式节约了资源。最大的问题是,每次访问都要进行线程同步(调用synchronized锁),实际上我们只在第一次调用该方法的时候才需要同步,一旦实例创建成功之后的同步完全没有必要。因此这种每次都需要同步的方法显然会造成不必要的同步开销

3.双重检查加锁

既然上面的方法有“每次访问都要进行线程同步”的问题,那我们就想了,是不是可以加一个条件把它限制在“只在第一次调用该方法的时候同步”?那么第一次调用getInstance()方法的时候,单例类显然还没有实例化,没有他自己的实例。因此,我们只要在创建该类实例的同步方法块外面加上一层“避免不必要的同步”的判断就行了,此时只要判断instance == null,就说明该方法肯定是第一次调用,此时我们再同步就避免了上述问题:

public class Singleton {
    private volatile static Singleton instance = null;
    private Singleton(){}
    public static Singleton getInstance(){
        if(instance == null){   //先检查实例是否存在,如果不存在才进入下面的同步块
            ①
            synchronized (Singleton.class) {    //同步块,线程安全的创建实例 
                ②
                if(instance == null){   //再次检查实例是否存在,如果不存在才真正的创建实例
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

可以看到,这种单例的特别之处在于对instance 进行了两次判空:第一层主要是为了避免不必要的同步,第二层则是为了在 null 的情况下创建实例。这样一来,我们解决了"每次调用同步"的问题但是此时又产生了一个问题——既然我们已经在外面判断了Singleton实例是否为空,那么在synchronized里面为什么还要再判断一次啊?这看起来完全就画蛇添足啊~~

(3.1)为什么要双重检查

1.第一层检查是为了避免不必要的同步:
  当instance实例已经存在时,系统调用getInstance()方法进入第一层判断。判断不为空不会进入synchronized代码块,直接返回instance对象。
2.第二层检查是为了在多个线程突破第一层检查时,仍然只创建一个实例
  假设最开始Instance对象不存在,线程A调用getInstance()方法,当该A线程运行到②位置时,此时又一个B线程也调用了getInstance()方法。因为A线程并没有执行instance = new Singleton();,此时instance仍然为空,因此B线程也能突破第一层非空判断,运行到①位置等待synchronized中的A线程执行完毕。
  当A线程释放同步锁时,instance已经非空。此时B线程从位置①开始执行到位置②。此时第二层非空判断就开始起作用了——由于有第二层非空判断,那么B线程在进行第二层非空判断的时候就不会通过,因此也不会创建多余的实例。试想一下加入没有第二层判断,那B线程岂不是顺利的重新创建了一个实例了。

(3.2)DCL(双重检查锁)失效问题

注意在上面代码中的第一句创建该类的空引用的时候有一个volatile关键字,这个关键字就引出了我们接下来要讲的双重检查锁定(Double Check Lock,简称DCL)失效问题。
  什么是DCL失效问题?简单来讲就是,获得锁的线程正在执行构造函数的时候,其他的线程执行到第一次检查if (m_instance == null)的时候,会返回false,因为已经在执行构造函数了,就不是null。因此,会把没有构造完全的对象返回给线程使用,这是不安全的。
  具体解释一下:instance = new Singleton();这句代码是一个"原子语句"(一组语句作为单独的不可分割的单元运行,中间不能被打断, 直到语句执行完毕),这句代码在运行的时候,分三个过程:

new操作之单线程流程.png

①给Singleton实例分配内存
②调用Singleton的构造函数,初始化成员字段
③将instance引用指向分配的内存(此时instance就非空了)
  但是在jdk1.5以前,由于java编译器允许处理器乱序执行,以及jdk1.5之前的JMM(java内存模型)中Cache,寄存器到主内存回写的顺序的规定,上面的第二和第三步的顺序是无法保证的。如果我们不巧碰到了乱序的情况,也就是第三部抢在了第二步执行之前,会出现什么问题呢?

DCL失效问题图解.png

如图,此时,由于先执行了第三部,instance已经不等于null,但是它还是一个非法的对象,因为还没有调用new进行初始化。此时B线程调用getInstance()时,会被挡在第一非空判断之外,并拿到这个非法的instance对象。这个时候就要出问题了,这就是DCL失效问题,而且这种难以跟踪难以重现的问题可能会隐藏很久。
  为了解决上面DCL失效问题,在JDK1.5及后续版本,将 private static Singleton instance=null; 改为 private volatile static Singleton instance=null; 避免DCL失效。
  DCL的优点:1.引入同步锁机制实现懒加载。2.避免不必要的同步。3.保证多线程下唯一性
缺点:1.JDK1.5以下版本小几率DCL失效问题。2.volatile会影响性能。
  volatile变量的读操作的性能消耗与普通变量没什么区别;但是写操作可能会变的慢一些,因为它需要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行。
  根据笔者的理解,虽然volatile关键字会影响性能,但是这种性能的影响(主要是保证“可见性”以及保证“指令的顺序执行”),对我们来讲并非到了一种不能容忍的地步,为了保证DCL中变量的合法性,我们还是应该主动使用volatile变量。更多关于volatile关键字以及Java内存模型的内容我们将另起一篇文章探讨。

4.静态内部类单例模式

解决DCL失效的另一种方法就是静态内部类。

public class Singleton {

    private Singleton(){}

    //静态的成员式内部类,该内部类的实例与外部类的实例没有绑定关系,而且只有被调用到时才会装载,从而实现了延迟加载
    private static class SingletonHolder{
        private static Singleton instance = new Singleton();    //静态初始化器,由JVM来保证线程安全
    }

    public static Singleton getInstance(){
        return SingletonHolder.instance;
    }
}

当getInstance()方法第一次被调用的时候,它第一次读取SingletonHolder.instance,导致SingletonHolder类得到初始化;而这个类在装载并被初始化的时候,会初始化它的静态域,从而创建Singleton的实例,由于是静态的域,因此只会在虚拟机装载类的时候初始化一次,并由虚拟机来保证它的线程安全性。

5.单例和枚举

就像文章开始所举的Retrofit的例子一样,这里给出枚举单例的一般写法:

public enum Singleton {
    INSTANCE;   //定义一个枚举的元素,它就代表了Singleton的一个实例。

    public void singletonOperation(){
        //功能处理
    }
}

按照《高效Java 第二版》中的说法:单元素的枚举类型已经成为实现Singleton的最佳方法。用枚举来实现单例非常简单,只需要编写一个包含单个元素的枚举类型即可。

今天突然感冒,有点严重,先写这么多吧~~日后再完善!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 151,829评论 1 331
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 64,603评论 1 273
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 101,846评论 0 226
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 42,600评论 0 191
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 50,780评论 3 272
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 39,695评论 1 192
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,136评论 2 293
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,862评论 0 182
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,453评论 0 229
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,942评论 2 233
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,347评论 1 242
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,790评论 2 236
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,293评论 3 221
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,839评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,448评论 0 181
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 34,564评论 2 249
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 34,623评论 2 249

推荐阅读更多精彩内容