聊聊设计模式中的那些坑 —— 单例模式(2)

静态初始化模式 ,直接看例子:

class EagerStaticSingleton {
    private EagerStaticSingleton(){
        System.out.println("EagerStaticSingleton");
    }
    private final static EagerStaticSingleton INSTANCE = new EagerStaticSingleton();

    public static EagerStaticSingleton getInstance(){
        return INSTANCE;
    }
}

这是最简单的静态初始化模式,可以看到类 EagerStaticSingleton 的内部具有一个静态(单例)常量 “INSTANCE”,它随着类在初始化阶段时就完成了实例的生成。

这种方法的优点是简单,你不必考虑多线程问题,因为虚拟机的类初始化机制本身就是线程安全的,因此无需为生成过程加锁。为了保证这个单例不被意外修改,它应该被声明为 final。但是它的缺点是可能造成内存泄漏,因为我们知道一个类被实例化之前需要先经过加载和初始化。通常虚拟机的加载器为了节约内存,提高执行的效率,仅在必要的时候才会执行相应的步骤(请参考《Java虚拟机规范》)。也就是说一个类被加载,不等于就会被初始化,被初始化不等就会被构建,同样被构建不等于会被使用。所以这个实现等于无论是否使用都强制执行了一次构建,这可能在内存资源紧张的系统中,或对于构建时间较长的类会遇到问题。

与之前的“懒加载”模式相对,这种不管用不用都生成实例的模式称为“饥饿”模式。顾名思义就是单例在静态初始化的同时就生成,而不管它是否真的被使用到。这个例子我们也可以写成以下形式:

class EagerStaticSingleton {
    private EagerStaticSingleton(){}
    private final static EagerStaticSingleton INSTANCE;
    static {
        INSTANCE = new EagerStaticSingleton();
    }

    public static EagerStaticSingleton getInstance(){
        return INSTANCE;
    }
}

相比上面的例子我们仅仅是把对象的初始化过程改到了静态初始化块中。通常情况下(单例对象的类型是一个类)这两者没有什么区别,但是如果单例对象是 Java 的内置类型或者 String,那么这两种写法就有区别了。我们知道 Java 在编译的时候,内置对象和String 类型的静态常量会被直接编译到静态常量池中,而静态常量池中的量在加载的同时就被直接赋予初始值,而不是等到初始化阶段才执行这个过程,它的执行效率会更高,并且可以越过初始化直接使用,不会产生效率或内存瓶颈问题。

class EagerStaticNativeTypeSingleton {
    private EagerStaticNativeTypeSingleton(){}
    public final static String INSTANCE = "final static INSTANCE";
    static {
        System.out.println("Static cinit");
    }
}

// 系统不会执行静态初始化块并且可以看到输出"final static INSTANCE"。
System.out.println(EagerStaticNativeTypeSingleton.INSTANCE);

为了避免饥饿模式的静态初始化单例的浪费和低效率,我们又有了以下懒加载模式的静态初始化单例:

class LazyStaticSingleton{
    private LazyStaticSingleton(){
        System.out.println("LazyStaticSingleton");
    }

    private static class Holder{
        private final static LazyStaticSingleton INSTANCE = new LazyStaticSingleton();
    }

    public static LazyStaticSingleton getInstance(){
        System.out.println("LazyStaticSingleton.getInstance() gets called");
        return Holder.INSTANCE;
    }
}

和饥饿模式相比,懒加载模式下单例的不会在类初始化过程中被强制实例化,我们可以通过以下测试比较以下:

Class.forName("pro.wangji.EagerStaticSingleton");  // “EagerStaticSingleton”
Class.forName("pro.wangji.LazyStaticSingleton");   // Nothing

我们会看到控制台只输出了“EagerStaticSingleton”而懒加载类不会输出任何信息,因为它的构造函数不会被触发,一直延迟到 getInstance() 方法被调用的时候,LazyStaticSingleton 才会尝试构造单例实例。

这种模式最有意思的地方是它不仅是懒惰的,而且也是线程安全的。因为内部静态类 Holder 取代了单例原本应该的位置,我们之前说过,类加载过程保证了静态成员的线程安全,而单例又是 Holder 的静态成员,因此 Holder 的初始化过程(被延迟到 getInstance() 被调用的那一刻)反过来又保证了单例的线程安全。

最后,在介绍完利用类静态初始化来完成单例之前,再介绍它的一个变种,枚举单例:

enum EnumSingleton {
    INSTANCE;
    public void method() {
        System.out.println("Enum singleton");
    }
}

枚举类在 JVM 中上被当作 final class 来看待,它也可以有构造函数和其它功能函数。并且它的枚举成员的类型就是它自己,也就是说以上相当于:

final class EnumSingleton {
    final static EnumSingleton INSTANCE = new EnumSingleton1();
    ...
}

毫无疑问,这是一个饥饿模式的静态初始化单例。

以上所有单例(包括第一章的内容)都是非序列化条件下的单例,如果我们的单例模型需要支持序列化,那么它们都还存在缺陷,因为当一个类被反序列化的时候JVM会生成一个全新的对象。


class SerializableSingleton implements Serializable {
    private SerializableSingleton(){}
    private final static SerializableSingleton INSTANCE = new SerializableSingleton();

    public static SerializableSingleton getInstance(){
        return INSTANCE;
    }
}

对于这样一个单例,我们测试以下它反序列化之后是否依然相等呢?

SerializableSingleton instance = SerializableSingleton.getInstance();

ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton_file"));
oos.writeObject(instance);

File file = new File("singleton_file");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
SerializableSingleton newInstance = (SerializableSingleton)ois.readObject();

System.out.println(instance == newInstance);  // false

所以,如果我们要得到一个支持反序列化的单例,以上方法还需要改进,我们需要给类加上一个特殊的方法:

private Object readResolve(){
    System.out.println("Avoid Serializable generates an new instance");
    return SerializableSingleton.getInstance();
}

readResolve() 方法在反序列化方法 readObject() 中被调用。如果一个类实现了这个方法,那么 readObject() 将优先执行这个方法来获得实例,否则执行内建的机制来生成新的实例。而在这个方法调用 getInstance() 来返回当前系统中已经存在的那个实例,因此避免了生成新的实例,就保证了系统中始终存在唯一的单例。

Leave a Reply
Your email address will not be published.
*
*

BACK TO TOP