Android开发中单例模式写法与可能遇到的坑

更新时间:2017-01-20 14:59:08 点击次数:2061次

年底了,手上的活不是很多,就想着将平时记录的笔记总结一下。准备总结一下平时常常使用的设计模式。本篇就是比较常用的单例(Singleton)模式。 
不管是Android开发还是Java开发,相信单例模式都是用的比较多的,平时再用的时候有没有想过,到底有多少种写法,或者有么有什么坑没有踩呢?带着这些问题我们先来了解一下什么情况下会用到单例模式。 
一般在希望系统中特定类只存在一个实例时,就可以使用单例模式。也就是说使用单例模式,关心的是对象创建的次数,以及对象创建的时机。它的UML图也是非常的简单:

结构很简单,但是我们再使用时,还是想要有一些要求的: 
1.在调用getInstance()方法时返回一个且的Singleton对象。 
2.能够在多线程使用时也能保证获取的Singleton对象 
3.getInstance()方法的性能要保证 
4.能在需要的时候才初始化,否则不用初始化 
 
现在就来按照上边的要求来实现吧。 
 
写法一 饿汉式 

/**
 * 饿汉式
 * 基于ClassLoader的机制,在同一classLoader下,该方式可以解决多线程同步的问题,
 * 但是该种单例模式没有办法实现懒加载
 */ public class SingletonHungry { /**
     * 在ClassLoader加载该类时,就会初始化mInstance
     */ private static SingletonHungry mInstance = new SingletonHungry(); private SingletonHungry() {
    } public static SingletonHungry getInstance() { return mInstance;
    }
 }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

以上就是饿汉式的写法,满足了上边说的第1,2条要求。该模式有几点要注意: 
1.默认构造方法需要私有化,不然外部可以随时的构造方法,这样就没法保证单例了。 
2.SingletonHungry 类型的静态变量mInstance也是私有化的。这样外部就不能直接获取到mInstance,并且正是由于mInstance是静态变量并且声明时就初始化了,我们知道根据java虚拟机和ClassLoader的特性,一个类在一个ClassLoader中只会被加载一次。并且这里的mInstance在加载时就已经初始化了,这可以确定对象的。也就是说保证了在多线程并发情况下获取到的对象是的。 
当然该种方式肯定也是有缺点的,就是不能满足上边要求中的第三点,例如某类实例需求依赖在运行时的参数来生成,那么由于饿汉式在类加载时就已经初始化了,所以无法满足懒加载。那我们就来看看懒加载的写法。

* 写法二 懒加载(非线程安全)* 

/**
 * 懒汉式
 * 只有在getInstance()时才会初始化mInstance
 * Created by chuck on 17/1/18.
 */ public class SingletonLazy { private static SingletonLazy mInstance; private SingletonLazy() {

    } public static SingletonLazy getInstanceUnLocked() { if (mInstance == null) {//line1 mInstance = new SingletonLazy();//line2 } return mInstance;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

可以看出确实是在调用getInstanceUnLocked()方法时,才会初始化实例,实现了懒加载。但是在能否满足在多线程下正常工作呢?我们在这里先分析一下假设有两个线程ThreadA和ThreadB: 
ThreadA首先执行到line1,这时mInstance为null,ThreadA将接着执行new SingletonLazy();在这个过程中如果mInstance已经分配了内存地址,但是还没有完成初始化工作(问题就出在这儿,稍后分析),如果ThreadB执行了line1,因为mInstance已经指向了某一内存,所以将跳过new SingletonLazy()直接得到mInstance,但是此时mInstance还没有完成初始化,那么问题就出现了。造成这个问题的原因就是new SingletonLazy()这个操作不是原子操作。至少可以分解成以下上个原子操作: 
1.分配内存空间 
2.初始化对象 
3.将对象指向分配好的地址空间(执行完之后就不再是null了) 
 
其中第2,3步在一些编译器中为了优化单线程中的执行性能是可以重排的。重排之后就是这样的: 
1.分配内存空间 
3.将对象指向分配好的地址空间(执行完之后就不再是null了) 
2.初始化对象 
重排之后就有可能出现上边分析的情况:

那么既然这个方式不能保证线程安全,那我们之间加上同步不就可以了吗?这确实也是一种方法

* 写法三 懒加载(线程安全)* 

/**
 * 懒汉式
 * 只有在getInstance()时才会初始化mInstance
 * Created by chuck on 17/1/18.
 */ public class SingletonLazy { private static SingletonLazy mInstance; private SingletonLazy() {

    } /**
 * 方法名多了Locked表示是线程安全的,没有其他意义
 */ public synchronized static SingletonLazy getInstanceLocked() { if (mInstance == null) {
            mInstance = new SingletonLazy();
        } return mInstance;
    }
 }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

这里和线程不安全的懒加载方式就是多了一个synchronized关键字,保证了线程安全,但是这又带来了另外一个问题,性能问题。如果,有多个线程会频繁调用getInstanceLocked()方法的话,可能会造成很大的性能损失。当然如果没有多线程频繁调用的话,就不存在多少性能损失了。那么为了解决这个问题,有人提出了我们非常熟悉的双重检查锁定(简称DCL)。

* 写法四 双重检查锁定(DCL)* 

/**
 * 双重检查锁定DCL
 * Created by chuck on 17/1/18.
 */ public class SingletonLazy { private static SingletonLazy mInstance; private SingletonLazy() {

    } public static SingletonLazy getInstance() { if (mInstance == null) {//次检查 synchronized (SingletonLazy.class) {//加锁 if (mInstance == null) {//第二次次检查 mInstance = new SingletonLazy();//new 一个对象 }
            }
        } return mInstance;
    }
 }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

在相当长的时间里,我以为这个完美的平衡了并发和性能的问题,但后来看多有文章介绍,这个方法也是有问题的,而这个问题和上边介绍过的重排问题一样。还是举ThreadA和ThreadB的例子: 
当Thread经过次检查对象为null时,会接着去加锁,然后去执行new SingletonLazy(),上边已经分析过了,改步骤存在重排现象,如果发生重排,即mInstance分配了内存地址,但是很没有完成初始化工作,而此时ThreadB,刚好执行次检查(没有加锁),mInstance已经分配了地址空间,不再为null,那么ThreadB会获取到没有完成初始化的mInstance,这就出现了问题。当然方法还是有的,那就是volatile关键字(用法自己查吧)。在JDK1.5之后使用volatile关键字,将禁止上文中的三步操作重排,既然不会重排,也就不会出现问题了。 
 

/**
 * 双重检查锁定DCL
 * Created by chuck on 17/1/18.
 */ public class SingletonLazy { private volatile static SingletonLazy mInstance; private SingletonLazy() {

    } public static SingletonLazy getInstance() { if (mInstance == null) {//次检查 synchronized (SingletonLazy.class) {//加锁 if (mInstance == null) {//第二次次检查 mInstance = new SingletonLazy();//new 一个对象 }
            }
        } return mInstance;
    }
 }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

问题是解决了,但是volatile要在JDK1.5以上版本(JDK1.5之前的可以参考http://www.ibm.com/developerworks/cn/java/j-dcl.html)才能起作用,其还会屏蔽jvm做的代码优化,这些有可能导致程序性能降低,并且目前为止DCL已经有一些复杂了。有没有更简单的方法呢?答案是有的

* 写法五 静态内部类* 

/**
 * 静态内部类方式实际上是结合了饿汉式和懒汉式的优点的一种方式
 * Created by chuck on 17/1/18.
 */ public class SingletonInner { private SingletonInner() {
    } /**
     * 在调用getInstance()方法时才会去初始化mInstance
     * 实现了懒加载
     *
     * @return */ public static SingletonInner getInstance() { return SingletonInnerHolder.mInstance;
    } /**
     * 静态内部类
     * 因为一个ClassLoader下同一个类只会加载一次,保证了并发时不会得到不同的对象
     */ public static class SingletonInnerHolder { public static SingletonInner mInstance = new SingletonInner();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

这是一个很聪明的方式,结合了结合了饿汉式和懒汉式的优点,并且也不影响性能。为什么这么说?因为我们在单例类SingletonInner类中,实现了一个static的内部类SingletonInnerHolder,该类中定义了一个static的SingletonInner类型的变量mInstance,并且会在classLoader次加载SingletonInnerHolder这个类时进行初始化。这样做的好处是在classLoader在加载单例类SingletonInner时不会初始化mInstance。只有在次调用SingletonInner的getInstance()方法时,classLoader才会去加载SingletonInnerHolder,并初始化mInstance,并且由于ClassLoader的机制,一个ClassLoader同一个类,只加载一次,那么不管多少线程,得到的也是同一个类,保证了并发下是该方式是可用的。其缺点也是有的,有些语言不支持这种语法。 
接下来在介绍一种很简单的方式: 
 
写法六 枚举 

/**
 * Created by chuck on 17/1/18.
 */ public enum SingletonEnum {
    SINGLETON_ENUM; private SingletonEnum() {
    }

    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

就是这么的简单,改方式不仅能避免多线程并发同步的问题,而且还天生支持序列化,可以防止在反序列化时创建新的对象。是一种比较推荐的方式,在java中需要在JDK1.5以上才支持enum。 
总结:单例模式还有其他的实现方法,熟悉Android的同学都知道,Handler机制中用到的ThreadLocal其实就使用了一种单例,就是在处理并发时,保证每一个线程都有一个单例实现。在上述介绍的各种方式中,没有哪一个是绝对好的,需要结合各自的情况决定。例如一般不要求懒加载的话,可以使用写法一饿汉式,如果要求懒加载,如果明确需要懒加载的,再根据是否需要线程安全考虑选择写法二,三。如果单例类需要反序列化,那么可以使用写法六枚举。总之,需要结合自己的实际情况来看。后,再来看看几个问题: 
、多ClassLoder情况,如果是多个ClassLoder都加载了单例类,那么就会出现多个同名的对象,这违背了单例模式的原则。解决这个问题,就要保证只有一个ClassLoder加载单例类。 
第二、单例类序列化问题,只要保证反序列化时,得到同一个对象就可以了,通过重写readResolve()方法可以实现。 

public class Singleton implements java.io.Serializable { ... private Object readResolve() { return mInstance;     
   }    
} 

本站文章版权归原作者及原出处所有 。内容为作者个人观点, 并不代表本站赞同其观点和对其真实性负责,本站只提供参考并不构成任何投资及应用建议。本站是一个个人学习交流的平台,网站上部分文章为转载,并不用于任何商业目的,我们已经尽可能的对作者和来源进行了通告,但是能力有限或疏忽,造成漏登,请及时联系我们,我们将根据著作权人的要求,立即更正或者删除有关内容。本站拥有对此声明的最终解释权。

回到顶部
嘿,我来帮您!