Android设计模式之单例模式Singleton

单例模式(Singleton)是使用最广的一种模式,在初学时必须要掌握并且运用的设计模式之一。
所谓单例,即是要创建唯一的实例。
在java程序开发中,我们会使用new构造的方法去创建对象实例,但是对于使用频繁的一些常用工具类,每次使用时都创建新的实例对象,就会造成大量的内存占用,从而产生垃圾。对于java程序而言,内存垃圾就是java程序的拖油瓶。
想要实现单例模式很简单,我们在初学时就会学习到常用的一两种单例创建方法,简单的去实现一个单例。
但是想要真正的实现只创建一个实例,那么就会涉及到很多高级知识,譬如多线程多进程的同时调用、线程安全、DCL双重锁检测(double checked locking)、多个类加载器(ClassLoader)协同时、跨JVM(集群、远程EJB等)时、单例对象被销毁后重建等。


实现单例,我们需要注意以下几点:
(1)私有化构造;
(2)对外提供单例类的实例获取接口;
(3)确保单例类的实例有且只有一个;
(4)确保单例类对象在反序列化时不会重复创建实例;

单例的实现方式:

1.懒汉式(线程不安全)

public class Singleton {

    private static Singleton single = null;

    private Singleton() {
    }

    /**
     * 获取单例对象实例
     */
    public static Singleton getInstance() {
        if (single == null) {
            single = new Singleton();
        }
        return single;
    }
}

这种单例实现方式实现起来比较简单,使用广泛,但是在多线程操作中可能会出现出现问题,可能会导致单例对象的重新创建。要想实现线程安全,我们就要用到synchronized关键字。

2.懒汉式(线程安全)

public class Singleton {

    private static Singleton single = null;

    private Singleton() {
    }

    /**
     * 获取单例对象实例
     */
    public static synchronized Singleton getInstance() {
        if (single == null) {
            single = new Singleton();
        }
        return single;
    }
}

使用同步方法的方式创建单例对象,在多线程并发时,同一时间只允许一个线程执行该方法,会起到保护方法不被多个线程同时调用的效果。
但是也会产生很多不必要的同步开销,所以一般不建议使用该创建方式。

3.饿汉模式

public class Singleton {

    private static Singleton single = new Singleton();

    private Singleton() {
    }

    /**
     * 获取单例对象实例
     */
    public static synchronized Singleton getInstance() {
        return single;
    }
}

饿汉模式在类创建时即会创建一个静态的实例,一直属于此类所有,便也是线程安全的。
但是它是饿汉式的,在ClassLoader加载类后实例就会第一时间被创建,在一些场景中将无法使用,如实例的创建是依赖参数或者配置文件的,在getInstance()之前必须调用某个方法设置参数给它,那样这种单例写法就无法使用了。

4.DCL(Double Checked Locking)双重校验模式

public class Singleton {

    private static Singleton single = null;

    private Singleton() {
    }

    /**
     * 获取单例对象实例
     */
    public static synchronized Singleton getInstance() {
        if (single == null) { //第一重校验
            synchronized (Singleton.class) {
                if (single == null) { //第二重校验
                    single = new Singleton();
                }
            }
        }
        return single;
    }
}

这种方式对single进行两次判空,第一次判断是为了避免不必要的同步,第二次判断是为了在null的情况下才创建实例。
但是在多线程并发时,假设线程一执行到single = new Singleton();,这里看起来是一句话,但实际上它并不是一个原子操作(原子操作的意思就是这条语句要么就被执行完,要么就没有被执行过,不能出现执行了一半这种情形)。事实上高级语言里面非原子操作有很多,我们只要看看这句话被编译后在JVM执行的对应汇编代码就发现,这句话被编译成8条汇编指令,大致做了3件事情:
1>给Singleton的实例分配内存;
2>初始化Singleton的构造器;
3>将single对象指向分配的内存空间(注意到这步single就非null了)。
由于Java编译器允许处理器乱序执行(out-of-order),以及JDK1.5之前JMM(Java Memory Medel)中Cache、寄存器到主内存回写顺序的规定,上面的第二点和第三点的顺序是无法保证的,也就是说,执行顺序可能是1-2-3也可能是1-3-2,如果是后者,并且在3执行完毕、2未执行之前,被切换到线程二上,这时候single因为已经在线程一内执行过了第三点,single已经是非空了,所以线程二直接拿走single,然后使用,然后顺理成章地报错,而且这种难以跟踪难以重现的错误估计调试上一星期都未必能找得出来,真是一茶几的杯具啊。

DCL的写法来实现单例是很多技术书、教科书(包括基于JDK1.4以前版本的书籍)上推荐的写法,实际上是不完全正确的。的确在一些语言(譬如C语言)上DCL是可行的,取决于是否能保证2、3步的顺序。在JDK1.5之后,官方已经注意到这种问题,因此调整了JMM、具体化了volatile关键字,因此如果JDK是1.5或之后的版本,只需要将single的定义改成private volatile static Singleton single = null;就可以保证每次都去single都从主内存读取,就可以使用DCL的写法来完成单例模式。当然volatile或多或少也会影响到性能,最重要的是我们还要考虑JDK1.42以及之前的版本。

5.静态内部类单例模式

public class Singleton {

    private static Singleton single = null;

    private Singleton() {
    }

    /**
     * 获取单例对象实例
     */
    public static final Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }

    /**
     * 静态内部类来创建实例
     */
    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }
}

第一次加载Singleton类的时候并不会初始化INSTANCE,只有第一次调用Singleton的getInstance()方法时才会初始化INSTANCE。因此,第一次调用getInstance()方法会导致虚拟机加载SingletonHolder 类,这种方式不仅能够确保单例对象的唯一性,同时也延迟了单例对象的实例化。

6.枚举单例

public enum Singleton {

    single; // 定义一个枚举元素,即为单例对象的一个实例

    public void doSomething(){
        // TODO doSomething ...
    }

}

枚举单例模式最大的优点就是写法简单,枚举在java中与普通的类是一样的,不仅可以有字段,还可以有自己的方法,而且枚举实例是线程安全的,在任何情况下它都是一个实例。即使是在反序列化的过程,枚举单例也不会重新创建新的实例。


推荐阅读《Android源码设计模式解析与实战》一书,指导学习。
本人经验尚浅,本文介绍稍显粗略,还望大牛多多指教。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值