阅读 32

单例模式总结

单例模式应该是所有设计模式中最简单的一种。但却算是最具争议的一个设计模式,有几种实现方式?哪种实现方式最好?不同的人可能有不同的理解。个人认为,如果仅按初始化的时机来区分,它其实只有两种实现方式:懒汉模式和饿汉模式。至于其他的Double-Check, 枚举,静态内部类其实都是这两种方式的变相实现而已。

懒汉模式

顾名思义,是一种有拖延症的实现方式(按需加载/延迟初始化),也就是说在使用单例对象时才对它进行初始化,标准的实现方式如下:

public class Singleton {
    
    private static Singleton INSTANCE = null;
    
    private Singleton() {
        
    }
    
    public static synchronized Singleton getInstance() {
        if (INSTANCE == null) {
            INSTANCE = new Singleton();
        }
        
        return INSTANCE;
    }
}
复制代码

但这种方式有点缺陷,每次获取对象时都是同步操作,影响代码的运行效率,所以就诞生了Double-Check版本,具体代码如下:

public class Singleton {

    private static volatile Singleton INSTANCE = null;

    private Singleton() {

    }

    public static Singleton getInstance() {
        if (INSTANCE == null) {
            synchronized (Singleton.class) {
                if (INSTANCE == null) {
                    INSTANCE = new Singleton();
                }
            }
        }

        return INSTANCE;
    }
}
复制代码

这种方式消除了对象被初始化之后获取实例时的同步操作,但实现稍显复杂。

volatile关键字可解决共享对象的可见性问题,通过它修饰的对象,数据发生变化时会直接会写到内存中,读取时JVM也会强制从内存中读取最新的值。

饿汉模式

饿汉模式采用的是类加载时就初始化实例的方式,这种方式的同步操作由JVM保证,所以操作更简单。代码实现如下:

public class Singleton {

    private static Singleton INSTANCE = new Singleton();

    private Singleton() {

    }

    public static Singleton getInstance() {
        return INSTANCE;
    }
}
复制代码

由于这种方式在类加载时就进行了初始化,所以可能出现未使用该实例,却对其进行了初始化,浪费内存。

类加载的时机:需要调用构造方法时被加载。

当然,也可使用更简单的实现方式——枚举:

public enum SingletonEnum {
    INSTANCE;
}
复制代码

其实枚举和类有很多相似之处,也可以定义构造方法和成员方法,如下:

public enum SingletonEnum {

// 实例的定义需与其构造方法的参数保持一致
INSTNACE(0);

private int value;

SingletonEnum(int value) {
    this.value = value;
}

public void printValue() {
    System.out.println(value);
}
复制代码

从上面的介绍来看,饿汉模式和懒汉模式各具优缺点,但却优劣互补,所以可以结合这两种方式——静态内部类实现单例。

public class Singleton {

    private Singleton() {
        
    }

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

    private static class SingletonHolder {
        private final static Singleton INSTANCE = new Singleton();
    }
}
复制代码

这是一种巧妙的实现方式,结合的饿汉模式和懒汉模式的优点,是一种比较推荐的实现方式。

实现方式的选择问题

对于饿汉模式和懒汉模式的选择,仁者见仁,智者见智。个人觉得没有绝对的标准,有时候还是需要结合业务开发来选择,在实际的开发中,使用单例模式的类时,基本都需要使用其实例,所在并不能说标准的饿汉模式就不能直接使用。

确保实例的唯一性

虽然使用上面的方式实现的类,正常情况下使用的总是同一个实例对象,但还是可以通过其他方式创建新的实例对象。

反射创建实例

即便构造方法是私有的,但还是可以通过反射的方式创建实例对象。

try {
    Class classZ = Class.forName("com.sxu.pattern.Singleton");
    Constructor constructor = classZ.getDeclaredConstructor();
    constructor.setAccessible(true);
    Object object = constructor.newInstance(null);
} catch (Exception e) {
    e.printStackTrace(System.out);
}
复制代码

所以为了避免反射创建新的实例对象,需要将构造方法进行修改,当单例类的实例已经存在时,需要在构造方式中抛出异常,避免实例对象的再次创建。

private Singleton() {
    if (SingletonHolder.INSTANCE != null) {
        throw new RuntimeException("Already exist a Singleton instance!");
    }
}
复制代码

反序列化创建实例

如果你的单例类是可序列化的,那么还要避免反序列化时创建实例对象的情况。虽然在构造方法中做了限制,确保对象不能通过反射来创建,但却不能避免反序列化创建实例对象,因为使用这种方式创建实例对象时,不会调用构造方法。关于序列化与反序列化,可参考序列化与反序列化这篇文章。

File file = new File("./", "object");
try {
    if (!file.exists()) {
        file.createNewFile();
    }
    // 将对象序列化到object文件中
    ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file));
    oos.writeObject(Singleton.getInstance());
    oos.close();

    // 将object文件中的数据反序列化到对象
    ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream(file));
    Object object = inputStream.readObject();
} catch (Exception e) {
    e.printStackTrace(System.out);
}
复制代码

通过打印log, 会发现反序列化得到的对象,地址和原来的地址是不同的。

对于Java类,序列化的时候会调用writeResolve方法,反序列化的时候会调用readResolve方法,默认情况会根据反序列化后的数据构造新对象,这里我们可通过重写readResolve方法来避免对象的重新创建。

private Object readResolve() throws IOException {
    return SingletonHolder.INSTANCE;
}
复制代码

通过clone对象创建实例

如果单例类实现了Cloneable接口,开发者也可通过clone方法直接创建对象(通过内存二进制流的拷贝),从而避开调用构造方法。所以需要对clone方法进行重写:

@Override
protected Object clone() throws CloneNotSupportedException {
    return SingletonHolder.INSTANCE;
}
复制代码

注意点

单例类的实例对象和应用有着同样的生命周期,一旦被创建,就会存在于整个应用的生命周期,所以在创建时如果需要引用其他的变量,其生命周期不能比应用的生命周期短,否则会出现内存泄漏。如Android开发中,在单例对象中引用Context对象时,如果传入的是Activity实例,就会出现Activity内存泄漏问题。

总结

单例模式是一种创建型的设计模式,其目的是为了避免频繁创建对象,节省内存资源,减少系统开销。其实也可认为它是一种编程思想,让开发者尽量避免频繁创建对象。