Android 常用设计模式(二) -- 单例模式(详解)

作者 : 夏至 欢迎转载,也请保留这段申明
http://blog.csdn.net/u011418943/article/details/60139644

上一篇讲到策略模式,变动的代码需要用到策略模式,感兴趣的小伙伴可以看看.
传送门:Android 常用设计模式之 – 策略模式

单例模式的定义就不解释过多了,相信很多小伙伴在设计的时候,都用到这个模式;常用的场景为 数据库的访问,文件流的访问以及网络连接池的访问等等,在这些场景中,我们都希望实例只有一个,除了减少内存开销之外,也防止防止多进程修改文件错乱和数据库锁住的问题。
在这一篇文章中,我将带你分析 android 常见的集中单例模式,并详细分析他们的优缺点。让大家在以后的选择单例中,可以根据实际情况选择。
当然,如有错误,也欢迎指正。
下面是介绍:

1、饿汉式

就是初始化的时候,直接初始化,典型的以时间换空间的做法。

public class SingleMode{
    //构造方法私有化,这样外界就不能访问了
    private SingleMode(){
    };
    //当类被初始化的时候,就直接new出来
    private static SingleMode instance = new SingleMode();
    //提供一个方法,给他人调用
    public static SingleMode getInstance(){
        return instance;
    }

}

饿汉式的好处是线程安全,因为虚拟机保证只会装载一次,再装载类的时候,是不会并发的,这样就保证了线程安全的问题。
但缺点也很明显,一初始化就实例占内存了,但我裤子还没脱,不想用呢。

2、懒汉式

为了解决上面的问题,开了懒汉式,就是需要使用的时候,才去加载;

public class SingleMode{
    //构造方法私有化,这样外界就不能访问了
    private SingleMode(){
    };
    private static SingleMode mSingleMode;
    public static SingleModegetInstance(){ //这里就是延时加载的意思
        if (mSingleMode == null){
            mSingleMode = new SingleMode();
        }
        return  mSingleMode;
    }
}

懒汉式如上所示,
优点:

我们只需要在用到的时候,才申请内存,且可以从外部获取参数再实例化,这点是懒汉式的最大优点了

缺点:

单线程只实例了一次,如果是多线程了,那么它会被多次实例

至于问什么说它是线程不安全的呢?先下面这张图:

《Android 常用设计模式(二) -- 单例模式(详解)》

我们假设一下,有两个线程,A和B都要初始化这个实例;此时 A 比较快,已经判断 mSingleMode 为null,正在创建实例,而 B 这时候也再判断,但此时 A 还没有 new 完,所以 mSingleMode 还是为空的,所以B 也开始 new 出一个对象出来,这样就相当于创建了两个实例了,所以,上面这种设计并不能保证线程安全。

2.1、如何实现懒汉式线程安全?

有人会说,简单啊,你既然是线程并发不安全,那么加上一个 synchronized 线程锁不就完事了?但是这样以来,会降低整个访问速度,而且每次都要判断,这个真的是我们想要的吗?

由于上面的缺点,所以,我们可以对上面的懒汉式加个优化,如双重检查枷锁:

public class SingleMode{
    //构造方法私有化,这样外界就不能访问了
    private SingleMode(){
    };
    private static SingleMode mSingleMode;
    public static SingleMode getInstance(){
        if (mSingleMode == null){
           synchronized (SingleMode.class){
               if (mSingleMode == null){  //二次检测
                   mSingleMode = new SingleMode();
               }
           }
        }
        return  mSingleMode;
    }
}

在上面的基础上,用了二次检查,这样就保证了线程安全了,它会先判断是否为null,是才会去加载,而且用 synchronized 修饰,则又保证了线程安全。

但是如果上面我们没有用 volatile 修饰,它还是不安全的,有可能会出现null的问题。为什么?这是因为 java 在 new 一个对象的时候,它是无序的。而这个过程我们假设一下,假如有线程A,判断为null了,这个时候它就进入线程锁了 mSingleMode = new SingleMode();,它不是一蹴而就,而是需要3步来完成的。

  • 1、为 mSingleMode 创建内存
  • 2、new SingleMode() 调用这个构造方法
  • 3、mSingleMode 指向内存区域

那你可能会有疑问,这样不是很正常吗?怎么会有 null 的情况?
非也,java 虚拟机在执行上面这三步的时候,并不是按照这样的顺序来的,可能会打乱,这儿就是java重排序,比如2和3调换一下:

  • 1、为 mSingleMode 创建内存
  • 3、mSingleMode 指向内存区域
  • 2、new SingleMode() 调用这个构造方法

那这个时候,mSingleMode 已经指向内存区域了,那这个时候它就不为 null了,而实际上它并未获得构造方法,比如构造方面里面有些参数或者方法,但是你并未获取,然而这个时候线程B过来,而 mSingleMode已经指向内存区域不为空了,但方法和参数并未获得, 所以,这样你线程B在执行 mSingleMode 的某些方法时就会报错。

当然这种情况是非常少见的,不过还是暴露了这种问题所在。
所以我们用volatile 修饰,我们都知道 volatile 的一个重要属性是可见性,即被 volatile 修饰的对象,在不同线程中是可以实时更新的,也是说线程A修改了某个被volatile修饰的值,那么我线程B也知道它被修改了。但它还有另一个作用就是禁止java重排序的作用,这样我们就不用担心出现上面这种null 的情况了。如下:

public class SingleMode{
    //构造方法私有化,这样外界就不能访问了
    private SingleMode(){
    };
    private volatile static SingleMode mSingleMode;
    public static SingleMode getInstance(){
        if (mSingleMode == null){
           synchronized (SingleMode.class){
               if (mSingleMode == null){  //二次检测
                   mSingleMode = new SingleMode();
               }
           }
        }
        return  mSingleMode;
    }
}

看到这里,是不是感觉爬了几百盘的坑,终于上了黄金段位了。。。
然而,并不是,你打了排位之后发现还是被吊打,所以我们可能还忽略了什么。
没错,这种方式,依旧存在缺点:
由于volatile关键字会屏蔽会虚拟机中一些必要的代码优化,所以运行效率并不是很高。因此也建议,没有特别的需要,不要大量使用。

笔者就遇到,使用这种模式,不知道什么原因,第二次进入 activity的时候,view 刷不出来,然而数据对象什么的都存在,调得我心力交瘁,欲生欲死,最后换了其他单例模式就ok了,希望懂的大侠告诉我一下,我只能怀疑volatile了。。。。。。

那你都这样说了,那还怎么玩,有没有一种更好的方式呢?别急,往下看。

3、静态式

什么叫静态式呢?回顾一下上面的饿汉式,我们再刚开始的就初始化了,不管你需不需要,而我们也说过,Java 再装载类的时候,是不会并发的,那么,我们能不能zuo做到懒加载,即需要的时候再去初始化,又能保证线程安全呢?当然可以,如下:

public class SingleMode{
    //构造方法私有化,这样外界就不能访问了
    private SingleMode(){
    };
    public static class Holder{
        private static SingleMode mSingleMode = new SingleMode();
        public static SingleMode getInstance(){
            return  mSingleMode;
        }
    }
}

除了上面的饿汉式和懒汉式,,静态的好处在于能保证线程安全,不用去考虑太多、缺点就在于对参数的传递比较不好。
那么这个时候,问题来了,参数怎么传递?这个确实没懒汉式方便,不过没关系,我们可以定义一个init()就可以了,只不过初始化的时候多了一行代码;如:

public class SingleMode {
    //构造方法私有化,这样外界就不能访问了
    private SingleMode(){
    };
    public static class Holder{
        private static SingleMode mSingleMode = new SingleMode();
        public static SingleMode  getInstance(){
            return  mSingleMode;
        }
    }
    private Context mContext;
    public void init(Context context){
        this.mContext = context;
    }
}

初始化:

 SingleMode mSingleMode = SingleMode.Holder.getInstance();
 mSingleMode.init(this);

4、枚举单例

java 1.4 之前,我们习惯用静态内部类的方式来实现单例模式,但在1.5之后,在 《Effective java》也提到了这个观点,使用枚举的优点如下:

  • 线程安全
  • 延时加载
  • 序列化和反序列化安全

所以,现在一般用单个枚举的方式来实现单例,如上面,我们改一下:

public static SingleMode getInstance(){
        return Singleton.SINGLETON.getSingleTon();
    }
    public enum Singleton{
        SINGLETON ; //枚举本身序列化之后返回的实例,名字随便取
        private AppUninstallModel singleton;

        Singleton(){ //JVM保证只实例一次
            singleton = new AppUninstallModel();
        }
        // 公布对外方法
        public SingleMode getSingleTon(){
            return singleton;
        }
    }

好吧,这样就ok了,但还是那个问题,初始化参数跟静态类一样,还是得重新写个 init() 有失必有得吧。

这样,我们的单例模式就学完了。

    原文作者:夏至的稻穗
    原文地址: https://blog.csdn.net/u011418943/article/details/60139644
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞