JAVA-单例模式

首先什么是单例?就一条基本原则,单例对象的类只会被初始化一次。在 Java 中,我们可以说在 JVM 中只存在该类的唯一一个对象实例。而要实现一个安全的单例对象,需要考虑一下几个问题:

  • 你的单例线程安全吗?

  • 你的单例反射安全吗?

  • 你的单例序列化安全吗?

一、单例的一般实现
1、饿汉式

public class HungrySingleton {

    private static final HungrySingleton mInstance = new HungrySingleton();

    private HungrySingleton() {
    }

    public static HungrySingleton getInstance() {
        return mInstance;
    }
}

私有构造器是单例的一般套路,保证不能在外部新建对象。饿汉式在类加载时期就已经初始化实例,由于类加载过程是线程安全的,所以饿汉式默认也是线程安全的。它的缺点也很明显,我真正需要单例对象的时机是我调用 getInstance() 的时候,而不是类加载时期。如果单例对象是很耗资源的,如数据库,socket 等等,无疑是不合适的。于是就有了懒汉式。

2、懒汉式

public class LazySingleton {

    private static LazySingleton mInstance;

    private LazySingleton() {
    }

    public static synchronized LazySingleton getInstance() {
        if (mInstance == null)
            mInstance = new LazySingleton();
        return mInstance;
    }
}

实例化的时机挪到了 getInstance() 方法中,做到了 lazy init ,但也失去了类加载时期初始化的线程安全保障。因此使用了 synchronized 关键字来保障线程安全。但这显然是一个无差别攻击,管你要不要同步,管你是不是多线程,一律给我加锁。这也带来了额外的性能消耗。这点问题肯定难不倒程序员们,于是,双重检查锁定(DCL, Double Check Lock) 应运而生。

3、DCL

public class DCLSingleton {
    private static DCLSingleton mInstance;
    private DCLSingleton() {
    }
    public static DCLSingleton getInstance() {
        if (mInstance == null) {                    // 1
            synchronized (DCLSingleton.class) {     // 2
                if (mInstance == null)              // 3
                    mInstance = new DCLSingleton(); // 4
            }
        }
        return mInstance;
    }
}

1 处做第一次判断,如果已经实例化了,直接返回对象,避免无用的同步消耗。2 处仅对实例化过程做同步操作,保证单例。3 处做第二次判断,只有 mInstance 为空时再初始化。看起来时多么的完美,保证线程安全的同时又兼顾性能。但是 DCL 存在一个致命缺陷,就是重排序导致的多线程访问可能获得一个未初始化的对象。

首先记住上面标记的 4 行代码。其中第 4 行代码 mInstance = new DCLSingleton(); 在 JVM 看来有这么几步:

1. 为对象分配内存空间
2. 初始化对象
3. 将 mInstance 引用指向第 1 步中分配的内存地址

在单线程内,在不影响执行结果的前提下,可能存在指令重排序。例如下列代码:
int a = 1;
int b = 2;
在 JVM 中你是无法确保这两行代码谁先执行的,因为谁先执行都不影响程序运行结果。同理,创建实例对象的三部中,第 2 步 初始化对象 和 第 3 步 将 mInstance 引用指向对象的内存地址 之间也是可能存在重排序的。

为对象分配内存空间
将 mInstance 引用指向第 1 步中分配的内存地址
初始化对象

这样的话,就存在这样一种可能。线程 A 按上面重排序之后的指令执行,当执行到第 2 行 将 mInstance 引用指向对象的内存地址 时,线程 B 开始执行了,此时线程 A 已为 mInstance 赋值,线程 B 进行 DCL 的第一次判断 if (mInstance == null) ,结果为 false,直接返回 mInstance 指向的对象,但是由于重排序的缘故,对象其实尚未初始化,这样就出问题了。还挺绕口的,借用 《Java 并发编程艺术》 中的一张表格,会对执行流程更加清晰。

时间 线程 A 线程 B
t1 A1: 分配对象的内存空间
t2 A3: 设置 mInstance 指向内存空间
t3 B1: 判断 mInstance 是否为空
t4 B2: 由于 mInstance 不为空,线程 B 将访问 mInstance 指向的对象
t5 A2: 初始化对象
t6 A3: 访问 mInstance 引用的对象 A3 和 A2 发生重排序导致线程 B 获取了一个尚未初始化的对象。

说了半天,该怎么改?其实很简单,禁止多线程下的重排序就可以了,只需要用 volatile 关键字修饰 mInstance 。在 JDK 1.5 中,增强了 volatile 的内存语义,对一个volatile 域的写,happens-before 于任意后续对这个 volatile 域的读。volatile 会禁止一些处理器重排序,此时 DCL 就做到了真正的线程安全。

4、静态内部类模式

public class StaticInnerSingleton {

    private StaticInnerSingleton(){}

    private static class SingletonHolder{
        private static final StaticInnerSingleton mInstance=new StaticInnerSingleton();
    }

    public static StaticInnerSingleton getInstance(){
        return SingletonHolder.mInstance;
    }
}

鉴于 DCL 繁琐的代码,程序员又发明了静态内部类模式,它和饿汉式一样基于类加载时器的线程安全,但是又做到了延迟加载。SingletonHolder 是一个静态内部类,当外部类被加载的时候并不会初始化。当调用 getInstance() 方法时,才会被加载。

枚举单例暂且不提,放在最后再说。先对上面的单例模式做个检测。

二、真的是单例?
还记得开头的提问吗?
你的单例线程安全吗?
你的单例反射安全吗?
你的单例序列化安全吗?

上面大篇幅的论述都在说明线程安全。下面看看反射安全和序列化安全。

1、反射安全
直接上代码,我用 DCL 来做测试:

public static void main(String[] args) {
    DCLSingleton singleton1 = DCLSingleton.getInstance();
    DCLSingleton singleton2 = null;

    try {
        Class clazz = DCLSingleton.class;
        Constructor constructor = clazz.getDeclaredConstructor();
        constructor.setAccessible(true);
        singleton2 = constructor.newInstance();
    } catch (Exception e) {
        e.printStackTrace();
    }
    System.out.println(singleton1.hashCode());
    System.out.println(singleton2.hashCode());
}

执行结果:
1627674070
1360875712
很无情,通过反射破坏了单例。如何保证反射安全呢?只能以暴制暴,当已经存在实例的时候再去调用构造函数直接抛出异常,对构造函数做如下修改:

private DCLSingleton() {
    if (mInstance!=null)
        throw new RuntimeException("想反射我,没门!");
}

上面的测试代码会直接抛出异常。

2、序列化安全
将你的单例类实现 Serializable 持久化保存起来,日后再恢复出来,他还是单例吗?

public static void main(String[] args) {

    DCLSingleton singleton1 = DCLSingleton.getInstance();
    DCLSingleton singleton2 = null;

    try {
        ObjectOutput output=new ObjectOutputStream(new FileOutputStream("singleton.ser"));
        output.writeObject(singleton1);
        output.close();

        ObjectInput input=new ObjectInputStream(new FileInputStream("singleton.ser"));
        singleton2= (DCLSingleton) input.readObject();
    } catch (Exception e) {
        e.printStackTrace();
    }
    System.out.println(singleton1.hashCode());
    System.out.println(singleton2.hashCode());

}

执行结果:

644117698
793589513
不堪一击。反序列化时生成了新的实例对象。要修复也很简单,只需要修改反序列化的逻辑就可以了,即重写 readResolve() 方法,使其返回统一实例。

protected Object readResolve() {
    return getInstance();
}

脆弱不堪的单例模式经过重重考验,进化成了完全体,延迟加载,线程安全,反射安全,序列化安全。全部代码如下:

public class DCLSingleton implements Serializable {

    private static DCLSingleton mInstance;

    private DCLSingleton() {
        if (mInstance!=null)
            throw new RuntimeException("想反射我,没门!");
    }

    public static DCLSingleton getInstance() {
        if (mInstance == null) {
            synchronized (DCLSingleton.class) {
                if (mInstance == null)
                    mInstance = new DCLSingleton();
            }
        }
        return mInstance;
    }

    protected Object readResolve() {
        return getInstance();
    }
}

三、枚举单例
在介绍利用枚举实现单例模式的原理,先介绍一些相关的基础内容。
首先,枚举类似类,一个枚举可以拥有成员变量,成员方法,构造方法。先来看枚举最基本的用法:

enum Type{
    A,B,C,D;
}

创建enum时,编译器会自动为我们生成一个继承自java.lang.Enum的类,我们上面的enum可以简单看作:

class Type extends Enum{
    public static final Type A;
    public static final Type B;
    ...
}

对于上面的例子,我们可以把Type看作一个类,而把A,B,C,D看作类的Type的实例。
当然,这个构建实例的过程不是我们做的,一个enum的构造方法限制是private的,也就是不允许我们调用。

“类”方法和“实例”方法

上面说到,我们可以把Type看作一个类,而把A,B。。。看作Type的一个实例。同样,在enum中,我们可以定义类和实例的变量以及方法。看下面的代码:

enum Type{
    A,B,C,D;
    static int value;
    public static int getValue() {
        return value;
    }
    String type;
    public String getType() {
        return type;
    }
}

在原有的基础上,添加了类方法和实例方法。我们把Type看做一个类,那么enum中静态的域和方法,都可以视作类方法。和我们调用普通的静态方法一样,这里调用类方法也是通过 Type.getValue()即可调用,访问类属性也是通过Type.value即可访问。
下面的是实例方法,也就是每个实例才能调用的方法。那么实例是什么呢?没错,就是A,B,C,D。所以我们调用实例方法,也就通过 Type.A.getType()来调用就可以了。
最后,对于某个实例而言,还可以实现自己的实例方法。再看下下面的代码:

enum Type{
A{
    public String getType() {
        return "I will not tell you";
    }
},B,C,D;
static int value;

public static int getValue() {
    return value;
}

String type;
public String getType() {
    return type;
 }
}

这里,A实例后面的{…}就是属于A的实例方法,可以通过覆盖原本的方法,实现属于自己的定制。
除此之外,我们还可以添加抽象方法在enum中,强制ABCD都实现各自的处理逻辑:

enum Type{
    A{
        public String getType() {
            return "A";
        }
    },B {
        @Override
        public String getType() {
            return "B";
        }
    },C {
        @Override
        public String getType() {
            return "C";
        }
    },D {
        @Override
        public String getType() {
            return "D";
        }
    };

    public abstract String getType();
}

枚举单例

有了上面的基础,我们可以来看一下枚举单例的实现方法:

public enum SomeThing {
    INSTANCE;
   public String get(){     return "SomeThing";} 
} 
main(){
    SomeThing something = SomeThing.INSTANCE;
    someThing.get(); 
}

上面的类Resource是我们要应用单例模式的资源,具体可以表现为网络连接,数据库连接,线程池等等。
获取资源的方式很简单,只要 SomeThing.INSTANCE.getInstance() 即可获得所要实例。下面我们来看看单例是如何被保证的:
首先,在枚举中我们明确了构造方法限制为私有,在我们访问枚举实例时会执行构造方法,同时每个枚举实例都是static final类型的,也就表明只能被实例化一次。在调用构造方法时,我们的单例被实例化。
也就是说,因为enum中的实例被保证只会被实例化一次,所以我们的INSTANCE也被保证实例化一次。
可以看到,枚举实现单例还是比较简单的,除此之外我们再来看一下Enum这个类的声明:

public abstract class Enum>
        implements Comparable, Serializable

可以看到,枚举也提供了序列化机制。某些情况,比如我们要通过网络传输一个数据库连接的句柄,会提供很多帮助。

你可能感兴趣的:(JAVA-单例模式)