设计模式(1)- 单例模式

前言

单例模式是一种比较简单也比较常用的模式。这种模式涉及到一个单一的类,该类负责创建自己的实例,同时也确保只有单个实例被创建,并有接口对外提供这个实例。一般单例模式多用于含有多个比较耗费资源的全局变量的工具类中。比如在一个Android应用中,应该只有一个ImageLoader实例,这个ImageLoader中含有线程池、缓存系统、网络请求等,创建多个的话很耗资源,就很没必要。下面讲几种比较常见的单例模式:

1.懒汉式

public class SingleTon {
    private static SingleTon instance ;

    private SingleTon(){}

    public static sysnchronized SingleTon getInstance() {
        if (instance == null) {
            instance = new SingleTon() ;
        }
        return instance ;
    }
}

SingleTon 类中先定义了一个静态对象,并且在第一次调用 getInstance 时初始化并返回该对象,后面每一次调用都会做一次判断。
懒汉式和饿汉式很容易记反有没有,可能都是褒义词的“近义词”吧。之前在看《Thinking In Java》一书中有提到 惰性初始化(Lazy Loading) 一词,就是让对象在将要使用之前的位置初始化,避免了内存浪费。2个放一起记就好记了,在第一次使用该单例对象的时候初始化的就是懒汉式。
懒汉式是通过来 synchronized 来加锁保证线程安全的,但是锁会影响效率,而且绝大多数情况下是不需要同步的。

2.饿汉式

public class SingleTon {
    private static final SingleTon instance = new SingleTon() ;

    private SingleTon(){}

    public static sysnchronized SingleTon getInstance() {
        return instance ;
    }
}

这种方式基于 ClassLoader 避免了多线程同步的问题,没有加锁,执行效率会提高。但是instance 在类装载的时候就实例化,浪费内存,很容易产生垃圾对象。关于类装载看一下这篇文章:单例模式之类的加载 。

3.双重检查锁(DLC, Double Check Lock)

public class SingleTon {
    private static final SingleTon instance = null ;

    private SingleTon(){}

    public static sysnchronized SingleTon getInstance() {
        if(instance == null) {
            sysnchronized(SingleTon.class) {
                if(instance == null) {
                    instance = new SingleTon() ;
                }
            }
        }
        return instance ;
    }
}

我的 synchronized / Lock+Volatile 这篇文章有关于java中3种锁的介绍,感兴趣的请移步看一下。
关于这里为什么要用到2个 if(instance == null)来判断,是因为 instance = new SingleTon() 这一句不是一个原子操作,用伪代码来表示分为三步:

inst = allocat() ;   // 分配内存 
constructor(inst) ;  // 执行构造函数
instance = inst ;    // 赋值

Java编译器允许处理器乱序执行,也就是可能会有指令冲排序发生,也就是说上面的第2、3步执行的顺序是无法保证的。也就是说执行顺序可能是1-2-3也可能是1-3-2.如果是1-3-2的话,当线程A执行了1-3时,instance已经不为 null 了,但是还没有调用构造函数初始化,这时候切换到线程B,B就直接取走了instance,在使用的时候就会出错了。当然 volatile 关键字也能保证原子操作不乱序执行,我上面的文章中有说明。

4.登记式(静态内部类)

public class SingleTon {

    private SingleTon(){}

    public static sysnchronized SingleTon getInstance() {
        return SingleTonHolder.instance ;
    }

    private static class SingleTonHolder {
        private static final SingleTon instance = new SingleTon() ;
    }
}

这种方式能达到双检锁方式一样的功效,但实现更简单。对静态域使用惰性初始化,应使用这种方式而不是双检锁方式。这种方式只适用于静态域的情况,双检锁方式可在实例域需要延迟初始化时使用。
这种方式同样利用了 classloder 机制来保证初始化 instance 时只有一个线程,它跟第 DCL 种方式不同的是:第 DCL 种方式只要 Singleton 类被装载了,那么 instance 就会被实例化(没有达到 lazy loading 效果),而这种方式是 Singleton 类被装载了,instance 不一定被初始化。因为 SingletonHolder 类没有被主动使用,只有显示通过调用 getInstance 方法时,才会显示装载 SingletonHolder 类,从而实例化 instance。想象一下,如果实例化 instance 很消耗资源,所以想让它延迟加载,另外一方面,又不希望在 Singleton 类加载时就实例化,因为不能确保 Singleton 类还可能在其他的地方被主动使用从而被加载,那么这个时候实例化 instance 显然是不合适的。这个时候,这种方式相比第 DCL 种方式就显得很合理。

5.枚举单例

public class SingleTon {
    INSTANCE ;

    public void whateverMethod(){ }
}

在上述的4中单例实现中,在反序列化的时候都可能会创建一个新的实例。当然也可以通过钩子函数 readResolve() 来阻止反序列化的时候重新生成对象。但是枚举最大的有点就是简单,自动支持序列化机制,而且能保证绝对的单例。但是枚举是在Java1.5之后才加入的新特性。

总结:

不管以哪种形式实现单例模式,核心原理都是 将构造函数私有化,并且通过静态方法来获取一个唯一的实例,在获取的过程中必须保证线程安全、防止反序列化重新生成实例。一般情况下,不建议使用懒汉方式,建议使用饿汉方式。只有在要明确实现 lazy loading 效果时,才会使用登记式。如果涉及到反序列化创建对象时,可以使用枚举方式。

参考书籍:

《Android源码设计模式解析与实战》

你可能感兴趣的:(设计模式(1)- 单例模式)