单例模式

来源:互联网 发布:免费行业报告下载知乎 编辑:程序博客网 时间:2024/06/05 20:52

单例模式,是我们最常用的设计模式之一,主要作用是保证在应用程序中,一个类Class只有一个实例存在。本篇文章主要是对常用的单例模式实现方式做一个总结。

饿汉模式


饿汉模式是我们比较常用的一种实现单例模式的方式之一

public class Singleton {    public static int VALUE = 1;    private static final Singleton INSTANCE = new Singleton();    private Singleton() {        Log.i("instance", "singleton constructor");    }    public static Singleton getInstance() {        return INSTANCE;    }}

通过静态属性初始化一个INSTANCE,提供getInstance()获取INSTANCE实例,而构造函数为无参私有函数,这样保证了实例在应用中只有一个实例(此处不考虑多个ClassLoader分别对Singleton进行加载)。
但是此种实现方式有一个很大的缺点,先看一段代码

 private void singletonTest() {     int value = Singleton.VALUE;     Log.i("instance", "singleton value:" + value); }

singletonTest()中访问Singleton.VALUE的值,但没有调用Singleton.getInstance()获取单例实例,看打印的log如下

I/instance: singleton constructorI/instance: singleton value:1

在访问Singleton.VALUE的时候,对Singleton进行了初始化,因为对类进行加载的时候,会对类的静态属性进行初始化,从而对Singleton进行实例化。
这样可能存在一个缺点:Singleton的实例化需要等待其他一些信息的加载时,而在信息没有加载完成的时候,调用了Singleton的某个静态属性间接导致对Singleton进行实例化,导致Singleton实例异常,甚至会影响程序的正常运行。
Singleton的实例化不依赖于外部时,可以采用此种饿汉方式实现单例模式

Double Check模式


上面介绍的饿汉模式在类进行加载的时候就对Singleton进行了实例化,不能做到延时实例化;而为了能够做到延时实例化,只有在调用到getInstance()的时候才进行实例化,下面的实现方式俗称懒汉模式,能够做到延时实例化

public class Singleton {    private static Singleton1 INSTANCE;    private Singleton() {        Log.i("instance", "singleton constructor");    }    public static Singleton getInstance() {        if(INSTANCE == null) {            INSTANCE = new Singleton();        }        return INSTANCE;    }}

getInstance()函数的里面进行判空处理,若为空,则进行初始化;但是这样的实现不是线程安全的,假如线程1执行到if(INSTANCE == null) 的时候,发现INSTANCE为空,从而准备对Singleton进行实例化;在线程1实例化未开始之前,INSTANCE依旧为空,此时刚好有一个线程2也执行到if(INSTANCE == null) ,发现此时INSTANCE为空,从而对Singleton进行实例化,导致Singleton不能保证只有一个实例。面对这种线程安全的问题,我们马上想到了同步,通过同步实现单例模式

public class Singleton {    public static Singleton INSTANCE;    private Singleton() {        Log.i("instance", "singleton constructor");    }    public static Singleton getInstance() {        synchronized (Singleton.class) {            if(INSTANCE == null) {                INSTANCE = new Singleton();            }        }        return INSTANCE;    }}

通过synchronized同步确实解决了上面懒汉模式中,因为线程安全可能会产生多个实例的问题,但是上面的实现代码还是会存在一些问题;当多个线程在执行getInstance()的时候,因为synchronized同步块,只有一个线程能访问其中的代码,其他的线程必须等候,当INSTANCE已经实例化,不为空的情况下,每个线程都只会读取INSTANCE,而不会进行实例化;但上面的代码都必须等待上一个获取锁的线程对INSTANCE判断是否为空,释放锁之后,才能让其他的一个线程获取锁,进行INSTANCE是否为空的判断,这会带来新的性能问题。
上面的实现方式在INSTANCE不为空的时候会带来性能问题,而懒汉模式在INSTANCE为空的时候会带来可能产生多个实例的问题,因此只需要在INSTANCE为空的时候,进行synchronized同步即可,这就是常用的实现单例模式方式之一的Double Check.

public class Singleton {    public static Singleton INSTANCE = null;    private Singleton() {        Log.i("instance", "singleton constructor");    }    public static Singleton getInstance() {        if(INSTANCE == null) {            synchronized (Singleton.class) {                if(INSTANCE == null) {                    INSTANCE = new Singleton();                }            }        }        return INSTANCE;    }}

上面的Double Check看上去确实没问题,但是真的没问题了吗?

JAVA中,JVM会对我们的代码进行优化,而其中之一就是指令重排序,非原子性的操作都可能会被JVM重排序,从而可能会带来新的问题。

INSTANCE = new Singleton()是非原子性的操作,在JVM中分为三个步骤执行:
1.在内存中为INSTANCE申请一块内存
2.通过构造函数Singleton()对它的成员变量进行初始化 (相当于new Singleton()
3.将步骤2中实例化的对象分配到步骤1中的内存中 (相当于INSTANCE = new Singleton(),此时开始INSTANCE不为null

因为JVM可能对指令进行重排序,执行的步骤可能是 1 → 3 → 2 或者 2 → 1 → 3,当步骤3在步骤2之前执行时,执行完步骤3后INSTANCE已经不为null,但是因为还没执行步骤2,导致INSTANCE中的一些成员变量还没有被初始化,此时若另外一个线程执行getInstance(),发现INSTANCE不为null,直接返回了一些成员变量还没有被初始化的INSTANCE,这样就可能因为这样INSTANCE而导致一些不可预知的问题。

JAVA中的关键字volatile能够禁止指令进行重排序,从而避免出现上述问题,最终的Double Check实现如下

public class Singleton {    public static volatile Singleton INSTANCE = null;    private Singleton() {        Log.i("instance", "singleton constructor");    }    public static Singleton getInstance() {        if(INSTANCE == null) {            synchronized (Singleton.class) {                if(INSTANCE == null) {                    INSTANCE = new Singleton();                }            }        }        return INSTANCE;    }}


内部类方式


在饿汉模式中提到,当类被加载时,类的静态属性会被初始化,并且类一个只会被加载一次,如果我们专门定义一个类来实现饿汉模式,既能保证只会出现一个实例和延时加载,同时也能避免出现线程安全的问题,这就是我们常用的内部类方式实现单例模式

public class Singleton {    private static class SingletonHolder {        private static final Singleton INSTANCE = new Singleton();    }    private Singleton (){}    public static final Singleton getInstance() {        return SingletonHolder.INSTANCE;    }}

这也是比较推崇的实现单例的方式之一


枚举方式


在Java引入枚举之后,可以通过枚举来实现单例模式

public enum Singleton{   INSTANCE;}

枚举在编译之后会生成对应的类,对应的枚举值会生成为静态属性,在枚举类被加载的时候会生成对应的实例,因为类只会被加载一次,因此能够实现实现单例模式,但是是因为在类加载的时候进行实例化的,所以同同样存在饿汉模式存在的问题。

总结


通过上面的分析,实现单例模式,推荐四种方式实现:饿汉模式、Double Check、内部类、枚举

饿汉模式 与 枚举方式 :在类被加载的时候进行实例化,不会延时初始化;在Singleton实例若依赖于其他信息的加载时,不推荐使用这两种方式加载。

Double Check 与 内部类方式:在首次调用getInstance()的时候进行加载,延时初始化;没有限制,适合所有场景使用。

其他实现单例模式的方式或多或少存在线程安全问题,不推荐使用

共同的问题

但是不管是饿汉模式、Double Check 、 内部类以及枚举的实现方式,都存在两个共同的问题:

1.反射:因为是私有构造函数,通过反射即可调用私有构造函数,实现多个实例

2.序列化:当把一个单例序列化之后再次反序列化就会得到一个新的对象,这样就实现了多个实例。

如何应对:在《Effect Java》中提到,为了维护并保证Singleton,必须声明所有实例域(成员变量)都是瞬时(transient)的,并提供一个readResolve方法。

 private Singleton readResolve() {    return INSTANCE; }