Android设计模式(一)单例模式详解

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u010289802/article/details/80117156
定义
确保某一个类的只有一个实例,而且自行实例化并向整个系统提供这个实例。
使用场景
    • 可以避免产生多个对象消耗过多的资源,如I/O访问等。
    • 某些类的对象就是应该只有一个,多个对象将导致逻辑错误或混乱。
常见的实现方式
下面是单例模式常见的两种实现方式 饿汉模式 双重锁模式
• 饿汉模式
public class Singleton {
    private static Singleton mInstance = new Singleton();
    private Singleton() { }
    public static Singleton getInstance() {
        return mInstance;
    }
}

饿汉式是最简单的实现方式,这种实现方式适合那些在初始化时就要用到单例的情况。简单粗暴,不管你用不用得着这个实例,先给你创建(new)出来。
如果单例对象初始化非常快,而且占用内存非常小的时候这种方式是比较合适的,可以直接在应用启动时加载并初始化。但是,如果单例初始化的操作耗时比较长而应用对于启动速度又有要求,或者单例的占用内存比较大,再或者单例只是在某个特定场景的情况下才会被使用,而一般情况下是不会使用时,使用饿汉式的单例模式就是不合适的,这时候就需要用到懒汉式的方式去按需延迟加载单例。

• 双重锁模式(DCL 实现)
public class Singleton {
    private static Singleton mInstance = null;
    private Singleton() { }
    public static Singleton getInstance() {
        if (mInstance == null) {
            synchronized (Singleton.class) {
                if (mInstance == null) {
                    mInstance = new LazySingleton();
                }
            }
        }
        return mInstance;
    }
}

还有一种就是懒汉模式,就是在用的时候才在getInstance() 方法中完成实例的创建(new),同时给这个方法添加synchronized 关键字,可以确保在多线程情况下单例依旧唯一,但是懒汉模式每次调用getInstance 方法时由于synchronized 的存在,需要进行同步,造成不必要的资源开销。因此便有了双重锁模式的实现方式。这样既避免了饿汉模式的缺点,又解决了懒汉模式的不足;确保单例只在第一次真正需要的时候创建。
DCL这种模式的亮点在于getInstance()方法上,其中对singleton 进行了两次判断是否空。
    • 第一层判断是为了避免不必要的同步
    • 第二层的判断是为了在null的情况下才创建实例。
具体我们来分析一下:

假设线程A执行到了singleton = new Singleton(); 语句,这里看起来是一句代码,但是它并不是一个原子操作,这句代码最终会被编译成多条汇编指令,它大致会做三件事情: 
(1)给Singleton的实例分配内存 
(2)调用Singleton()的构造函数,初始化成员字段; 
(3)将singleton对象指向分配的内存空间(即singleton不为空了);
但是由于Java编译器允许处理器乱序执行,以及在jdk1.5之前,JMM(Java Memory Model:java内存模型)中Cache、寄存器、到主内存的回写顺序规定,上面的步骤b 步骤c的执行顺序是不保证了。也就是说执行顺序可能是1-2-3,也可能是1-3-2,如果是后者的指向顺序,并且恰恰在3执行完毕,2尚未执行时,被切换到线程B中,这时候因为singleton在线程A中执行了步骤3了,已经非空了,所以,线程B直接就取走了singleton,再使用时就会出错。这就是DCL失效问题。 

但是在JDK1.5之后,官方给出了volatile关键字,将singleton定义的代码改成:

private volatile static Singleton singleton;  //使用volatile 关键字

这样就解决了DCL失效的问题。


更多
在某些复杂的场景中,上述的两种方式都或多或少的存在一些缺陷。因此便有了以下两种单例模式的实现方式。
• 静态内部类
public class Singleton {
   private Singleton(){ }
   public static Singleton getInstance(){
       return SingletonHolder.sInstance;
   }

   /**
    * 静态内部类
    */
   private static SingletonHolder{
       private static final Singleton sInstance = new Singleton();
   }
}

当第一次加载 Singleton 类时,并不会初始化 sInstance,只有在第一次调用 Singleton 的 getInstance 方法时才会导致 sInstance 被初始化。因此,第一次调用 getInstance 方法会导致虚拟机加载 SingletonHolder 类,这种方式不仅能够确保线程安全,也能够保证单例对象的唯一性,同时也延迟了单例的实例化,所以这是推荐使用的单例模式的实现方式。

• 枚举单例
publicenum  Singleton {
    //定义一个枚举的元素,它就是Singleton的一个实例
    INSTANCE;
    public void doSomething(){
        //do something
    }
}

定义一个枚举元素,而他就是单例;可以说,这是实现单例最简单最实惠的方式;可以有效的避免单例在反序列化的过程中被创建,从而让单例变得不唯一。默认枚举实例的创建是线程安全的(创建枚举类的单例在JVM层面也是能保证线程安全的), 所以不需要担心线程安全的问题,所以理论上枚举类来实现单例模式是最简单的方式。

不可否认枚举会使得代码更易读更安全,我们在很多经典的Java书已经看到推荐使用枚举来代替int常量了,但是Android官网不建议使用枚举,因为占用内存多。特别是大型的App中,能不用则不用。因为它会牺牲执行的速度和并大幅增加文件体积。这也是性能优化中减少OOM的一个方面


总结

单例模式是运用频率很高的模式,但是,由于在客户端通常没有高并发的情况,因此,选择哪种实现方式并不会有太大影响。即便如此,出于效率考虑,推荐用DCL、静态内部类的形式。

缺点

(1)单例模式一般没有借口,扩展很困难,若要扩展,除了修改代码基本上没有第二种途径可以实现。
(2)单例对象如果持有Context,那么很容易引发内存泄漏,此时需要注意传递给单例对象的Context最好是Application Context。

猜你喜欢

转载自blog.csdn.net/u010289802/article/details/80117156