android设计模式—单例设计模式

版权声明:本文为博主原创文章,转载请注明地址。 https://blog.csdn.net/huangxiaoguo1/article/details/88054799

适用场景:

单例模式只允许创建一个对象,因此节省内存,加快对象访问速度,因此对象需要被公用的场合适合使用,如多个模块使用同一个数据源连接对象等等。如: 
1.需要频繁实例化然后销毁的对象。 
2.创建对象时耗时过多或者耗资源过多,但又经常用到的对象。 
3.有状态的工具类对象。 
4.频繁访问数据库或文件的对象。 

以下都是单例模式的经典使用场景:

1.资源共享的情况下,避免由于资源操作时导致的性能或损耗等。如日志文件,应用配置。 
2.控制资源的情况下,方便资源之间的互相通信。如线程池等。 

要点

  • 构造函数不对外开放,必须为Private(就是不能用New的形式生成对象)
  • 通过一个静态方法或者枚举返回单例对象
  • 确保单例类的对象有且只有一个,尤其是在多线程环境下
  • 确保单例类对象在反序列化时不会重新创建对象

饿汉单例模式

public class Singleton {
    private static final Singleton singleton = new Singleton();
    //构造函数私有化
    private Singleton() {
    }
    //公有的静态函数,对外暴露获取单例对象的接口
    public static Singleton getInstance() {
        return singleton;
    }
}

饿汉单例模式采用的是静态变量 + fianl关键字的方式来确保单例模式,应用启动的时候就生成单例对象,效率不高

懒汉模式

public class Singleton {
    private static Singleton singleton;
    //构造函数私有化
    private Singleton() {
    }
    //公有的静态函数,对外暴露获取单例对象的接口
    public static synchronized Singleton getInstance() {
        if (singleton == null) {
            singleton = new Singleton();
        }
        return singleton;
    }
}

懒汉模式的主要是加了synchronized关键字,每调用一次getInstance方法,都会进行同步,造成了不必要的开销

DCL模式(双重检查锁定模式)

public class Singleton{   
    // 静态属性,volatile保证可见性和禁止指令重排序
    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;   
    }   
}
  • DCL模式是使用最多的单例模式,它不仅能保证线程安全,资源利用率高,第一次执行getInstance时单例对象才会实例化;同时,后续调用getInstance方法时又不会有懒汉模式的重复同步的问题,效率更高;在绝大多数情况下都能保证单例对象的唯一性
  • DCL模式需要注意要用volatile关键字,否则还是会导致创建多个实例
  • DCL模式的缺点是第一次加载时由于需要同步反应会稍慢;在低于JDK1.5的版本里由于Java内存模型的原因有可能会失效
volatile
volatile 是DCL模式的关键,他保证了可见性和禁止指令重排序,一般情况下android程序员都会忘记加volatile!!!!
  • 禁止指令重排序。 我们知道new Singleton()是一个非原子操作,编译器可能会重排序【构造函数可能在整个对象初始化完成前执行完毕,即赋值操作(只是在内存中开辟一片存储区域后直接返回内存的引用)在初始化对象前完成】。而线程B在线程A赋值完时判断instance就不为null了,此时B拿到的将是一个没有初始化完成的半成品。
  • 保证可见性。 线程A在自己的工作线程内创建了实例,但此时还未同步到主存中;此时线程B在主存中判断instance还是null,那么线程B又将在自己的工作线程中创建一个实例,这样就创建了多个实例。

什么是指令重排序?有两个层面:

  1. 在虚拟机层面,为了尽可能减少内存操作速度远慢于CPU运行速度所带来的CPU空置的影响,虚拟机会按照自己的一些规则(这规则后面再叙述)将程序编写顺序打乱——即写在后面的代码在时间顺序上可能会先执行,而写在前面的代码会后执行——以尽可能充分地利用CPU。拿上面的例子来说:假如不是a=1的操作,而是a=new byte1024*1024`,那么它会运行地很慢,此时CPU是等待其执行结束呢,还是先执行下面那句flag=true呢?显然,先执行flag=true可以提前使用CPU,加快整体效率,当然这样的前提是不会产生错误(什么样的错误后面再说)。虽然这里有两种情况:后面的代码先于前面的代码开始执行;前面的代码先开始执行,但当效率较慢的时候,后面的代码开始执行并先于前面的代码执行结束。不管谁先开始,总之后面的代码在一些情况下存在先结束的可能。

  2. 在硬件层面,CPU会将接收到的一批指令按照其规则重排序,同样是基于CPU速度比缓存速度快的原因,和上一点的目的类似,只是硬件处理的话,每次只能在接收到的有限指令范围内重排序,而虚拟机可以在更大层面、更多指令范围内重排序。硬件的重排序机制参见《从JVM并发看CPU内存指令重排序(Memory Reordering)》
    重排序很不好理解,上面只是简单地提了下其场景,要想较好地理解这个概念,需要构造一些例子和图表,在这里介绍两篇介绍比较详细、生动的文章《happens-before俗解》和《深入理解Java内存模型(二)——重排序》。其中的“as-if-serial”是应该掌握的,即:不管怎么重排序,单线程程序的执行结果不能被改变。编译器、运行时和处理器都必须遵守“as-if-serial”语义。

    引用于:java指令重排序的问题

静态内部类单例模式

public class Singleton {
    private Singleton() {
    }
    
    public static Singleton getInstance() {
        return SingletonHolder.sInstance;
    }
    
    //静态内部类
    private static class SingletonHolder {
        private static final Singleton sInstance = new Singleton();
    }
}
  • 第一次加载Singleton类时不会初始化sInstance,只有在第一次调用getInstance方法时才会初始化sInstance,延迟了单例对象的实例化

  • 静态内部类单例模式不仅能保证线程安全也能保证单例对象的唯一性

    推荐使用饿汉模式和静态内部类方式实现单例模式

枚举单例

    public enum Singleton {
        INSTANCE;
    }
  • 默认枚举实例的创建是线程安全的,并且在任何情况下它都是一个单例

  • 其他的单例模式,在一种情况下会出现失效的情况——反序列化,但是枚举即使在反序列化情况下也不会失效

总结

  • 推荐用DCL模式和静态内部类2种实现。

  • 单例对象的生命周期很长,如果持有Context,很容易引发内存泄漏,所以传递给单例对象的Context最好是Application Context

  • 如果设计到反序列化使用枚举单例

猜你喜欢

转载自blog.csdn.net/huangxiaoguo1/article/details/88054799