[Android]单例模式

概述

单例模式是应用最广的模式之一,在应用这个模式时,单例对象的类必须保证只有一个实例存在。许多时候整个系统只须要拥有一个全局对象,这样有利于咱们协调系统总体的行为。如在一个应用中,应该只有一个ImageLoader实例,这个ImageLoader中又含有线程池、缓存系统、网络请求等,很消耗资源。所以不该该让它构造多个实例。这样不能自由构造对象的状况,就是单例模式的使用场景。数据库

定义

确保一个类只有一个实例,并且自行实例化并向整个系统提供这个实例。编程

使用场景

确保某个类有且只要一个对象的场景,避免产生多个对象消耗过多的资源,或者某种类型的对象只应该有且只有一个。例如,建立一个对象须要消耗的资源过多,如要访问IO和数据库等资源,这时就要考虑使用单例模式。设计模式

UML类图

单例模式的UML类图以下:缓存

角色介绍:安全

  • Client:高层客户端网络

  • Singleton:单例类多线程

实现单例模式主要有如下几个关键点:并发

  • 构造函数不对外开放,通常为private;ide

  • 经过一个静态方法或者枚举返回单例类对象;函数

  • 确保单例类的对象有且只有一个,尤为是在多线程环境下;

  • 确保单例类对象在反序列化时不会从新构建对象;

单例模式中实现比较困难的是在多线程环境下构造单例类的对象有且只有一个。

简单示例

单例模式在设计模式中是结构比较简单的,只有一个单例类,没有其余层次结构和抽象。该模式须要确保该类只能生成一个对象,一般是该类须要消耗较多的资源或者没有对个实例的状况。例如一个公司只有一个CEO、一个应用只有一个Application对象等。

下面以公司里的CEO为例来简单演示一下,一个公司能够有多个VP、无数个员工,但只有一个CEO,代码以下:

/**
 * 
 * 普通员工
 *
 */
public class Staff {
    
    public void work() {
        //干活
    }


}

//副总裁
public class VP extends Staff {

    @Override
    public void work() {
        // 管理下面的经理
        
    }
}

//CEO,饿汉式单例
public class CEO extends Staff {
    
    private static final CEO mCEO = new CEO();
    
    private CEO() {
        
    }

    //公有的静态函数,对外暴露获取单例对象的接口
    public static CEO getCeo() {
        return mCEO;
    }
    @Override
    public void work() {
        // 管理VP
    }
}

//公司类
public class Company {

    private List<Staff> mStaffs = new ArrayList<Staff>();
    
    public void addStaff(Staff staff) {
        mStaffs.add(staff);
    }
    
    public void showStaffs() {
        for(Staff staff : mStaffs) {
            System.out.println("Obj: " + staff.toString());
        }
    }
}

    public static void main(String[] args) {
        // TODO Auto-generated method stub
        
        Company company = new Company();
        
        //CEO对象只能经过getCeo获取
        Staff ceo1 = CEO.getCeo();
        Staff ceo2 = CEO.getCeo();
        company.addStaff(ceo1);
        company.addStaff(ceo2);
        
        Staff vp1 = new VP();
        Staff vp2 = new VP();
        company.addStaff(vp1);
        company.addStaff(vp2);
        
        Staff staff1 = new Staff();
        Staff staff2 = new Staff();
        
        company.addStaff(staff1);
        company.addStaff(staff2);

        company.showStaffs();
    }

运行输出结果以下:

Obj: com.liuguoquan.design.single.CEO@15db9742
Obj: com.liuguoquan.design.single.CEO@15db9742
Obj: com.liuguoquan.design.single.VP@6d06d69c
Obj: com.liuguoquan.design.single.VP@7852e922
Obj: com.liuguoquan.design.single.Staff@4e25154f
Obj: com.liuguoquan.design.single.Staff@70dea4e

从上面代码能够看出,CEO类不能经过new的形式构造函数,只能经过CEO.getCeo()方法来获取,而这个CEO对象是静态对象,而且在声明的时候就已经初始化,这就保证类CEO对象的惟一性。

从输出结果中能够看出,CEO两次输出的CEO对象的地址都同样,说明是同一个CEO对象;而VP、Staff等类型的对象都是不一样的。

实现方式

饿汉式

饿汉式模式是在声明静态对象时就已经初始化,这种方式简单粗暴,若是单例对象初始化很是快,并且占用内存小的时候这种方式是比较适合的,能够直接在应用启动时加载初始化。实现以下:

public class Singleton {

    private static Singleton instance = new Singleton();

    private Singleton() {

    }

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

懒汉式

懒汉模式是声明一个静态对象,而且在用户第一次调用getInstance时进行初始化。

public class Singleton {

    private static Singleton instance = null;

    private Singleton() {

    }

    public static Singleton getInstance() {

        synchronized(Singleton.class) {
            if (instance == null) {
                instance = new Singleton();
            }
        return instance;
    }
}

getInstance方法中添加了Synchronized关键字,也就是同步类synchronized关键字包含的代码块,这就是上面所说的在多线程中保证单例对象惟一性的手段。可是仍存在一个问题,即便instance已经初始化,每次调用getInstance方法都会进行同步,这样会消耗没必要要的资源,这也是懒汉式存在的最大问题

懒汉单例模式的优势是只有在使用时才会被实例化,在必定程度上节约了资源,缺点是第一次加载时须要及时进行实例化,反应稍慢,最大问题是每次调用geInstance都进行同步,形成没必要要的同步开销,这样模式通常不建议使用。

Double CheckLock(双重校验锁)

DCL方式的优势是既可以在须要时才初始化单例,又可以保证线程的安全,且单例对象初始化后调用getInstance不获取同步锁。

public class Singleton {

    //private static volatile Singleton instance = null;
    private static Singleton instance = null;

    private Singleton() {

    }

    public static Singleton getInstance() {
        //若是已经初始化,不须要每次获取同步锁
        if(instance == null) {
            synchronized(Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
        }
        return instance;
    }
}

能够看到getInstance方法对instance进行了两次判空:第一层判断主要是为了不没必要要的同步,第二层判断主要则是为了在null的状况下建立实例。下面,咱们来分析一下:

假设线程A执行到instance=new Singleton()语句,这里看起来是一句代码,但实际上它并非一个原子操做,这局代码最终会被编译成多条汇编指令,它大体作了3件事情:

  1. 给Singleton的实例分配内存

  2. 调用Singleton()的 构造函数,初始化字段成员

  3. 将instance对象执行分配的内存空间(此时instance就不是null了)

可是,因为Java编译器运行处理器乱序执行,以及jdk1.5以前Java内存模型中Cache、寄存器到主内存会写顺序的规定,上面的第二和第三的顺序是没法保证的。也就是说,执行顺序多是1-2-3也多是1-3-2.若是是后者,而且在3执行完毕、2未执行以前,被切换到线程B上,这时候instance由于已经在线程A内执行3了,instance已是非null,全部线程B直接取走instance,再使用时就会出错,这就是DCL失效问题,并且这种难以跟踪难以重现的问题极可能会隐藏好久。

在jdk1.5以后,官方已经注意到这种问题,调整了JMM、具体化了volatile关键字,所以,若是是1.5或以后的版本,只须要将instance的定义改为private static volatile Singleton instance = null;就能够保证instance对象每次都是从主内存中读取,就可使用DCL的写法来完成单例模式。固然,volatile多少会影响到性能,但考虑到程序的正确性,牺牲这点性能仍是值得的。

DCL的优势:资源利用率高,第一次执行getInstance时单例对象才会被实例化,效率高。

缺点:第一次加载稍慢,也因为Java内存模型的缘由偶尔会失败。在高并发的环境下也有必定的缺陷,虽然几率发生很小。

DCL模式是使用最多的单例实现模式,它可以在须要时才实例化单例对象,而且可以在绝大多数场景下保证单例对象的惟一性,除非你的代码在并发场景比较复杂或者低于jdk1.6版本下使用,不然这种方式通常可以知足需求。

静态内部类单例模式

在《Java并发编程实战》中谈到不同意使用DCL的优化方式,而建议使用以下代码替代:

public class Singleton {

    private Singleton() {}

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

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

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

枚举单例

public enum Singleton {

    //定义一个枚举的元素,它就是Singleton的一个实例
    INSTANCE;

    public void doSomething() {

    }
}

//使用
public static void main(String[] args){
   Singleton singleton = Singleton.instance;
   singleton.doSomething();
}

写法简单是枚举单例最大的优势,枚举在Java中与普通的类是同样的,不只可以有字段,还可以有本身的方法。最重要的是默认枚举实例的建立时线程安全的,而且在任何状况下它都是一个单例。

为何这么说呢?在上述的几种单例模式实现中,在一个状况下它们会出现从新建立对象的状况,那就是反序列化。

经过序列化能够将一个单例的实例对象写到磁盘,而后再读回来,从而有效地得到一个实例。即便构造函数时私有的,反序列化时依然能够经过特殊的途径去建立类的一个新的实例,至关于调用该类的构造函数。反序列化操做提供一个很特别的钩子函数,类中具备一个私有的、被实例化的方法readResolve(),这个方法可让开发人员控制对象的反序列化。例如,上述几个实例中若是要杜绝单例对象在被反序列化时从新生成对象,那么必须加入以下方法:

private Object readResolve() throws ObjectStreamException {
    return instance;
}

也就是在readResolve方法中将instance对象返回,而不是默认的从新生成一个新的对象。而对于枚举并不存在这样的问题,由于即便反序列化它也不会从新生成新的实例。

容器管理单例

public class SingletonManager {

    private static Map<String,Object> objMap = new HashMap<String,Object>();

    public static void registerService(String key,Object instance) {
        if (!objMap.containsKey(key)) {
            objMap.put(key);
        }
    }

    public static getInstance(String key) {
        return objMap.get(key);
    }
}

在程序的初始,将多种单例类注入到一个统一的管理类中,在使用根据key获取对应类型的对象,这种方式使得咱们能够管理不少类型的单例,而且在使用它们的时候能够经过统一的接口进行获取操做操做,下降用户的使用成本,也对用户隐藏了具体实现,下降了耦合度。

总结

单例模式是运用频率很高的模式,可是,因为在客户端一般没有高并发的状况,所以,选择哪一种实现方式并不会有太大的影响。即使如此,出于效率考虑,推荐使用双重校验锁和静态内部类单例模式。

优势

  • 因为单例模式在内存中只有一个实例,减小了内存开支,特别是一个对象须要频繁建立、销毁时,并且建立或者销毁时性能又没法优化,单例模式的优点就很是明显。

  • 因为单例模式只生成一个实例,因此,减小了系统的性能开销,当一个对象的产生须要比较多的资源时,如读取配置、产生依赖对象时,则能够经过在应用启动时直接产生一个单例对象,而后用永驻内存的方式解决。

  • 单例模式能够避免对资源的多重占用,例如一个写文件操做,因为只有一个实例存在内存中,避免对同一个资源文件的同时写操做。

  • 单例模式能够在系统设置全局访问点,优化和共享资源访问,例如,能够设计一个单例类,负责全部数据表的映射处理。

缺点:

  • 单例模式通常没有接口,扩展很困难,若要扩展,除了修改代码基本没有第二种途径能够实现。

  • 在Android中,单例对象若是持有Context,那么很容易引起内存泄露,此时须要注意传给单例对象的Context最好是Application Context。

相关文章
相关标签/搜索