【Java】枚举实现单例模式

Posted by 西维蜀黍 on 2019-03-26, Last Modified on 2021-09-21

背景

单例模式的不同实现

双重检验锁(double-check locking)

public class Singleton {
    private volatile static Singleton instance; //声明成 volatile
    private Singleton (){}

    public static Singleton getSingleton() {
        if (instance == null) {                         
            synchronized (Singleton.class) {
                if (instance == null) {       
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

饿汉式 static final field

public class Singleton{
    //类加载时就初始化
    private static final Singleton instance = new Singleton();
    
    private Singleton(){}

    public static Singleton getInstance(){
        return instance;
    }
}

静态内部类 static nested class

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

分析

以上列举了三种单例模式的正常实现。

不知道你有没有注意到,上面的三种实现方法都包含了一个private的构造函数。因此,这是不是意味着,我们就能保证无法创建多个类的实例了呢?

答案是否定的,即我们仍然有其他的高阶方法来创建多个类的实施,以破解单例模式。

  • 序列化(serialization)/反序列化(deserializations)
  • 反射(reflection)

序列化(serialization)/反序列化(deserializations)问题

序列化可能会破坏单例模式。

通过比较一个序列化后的对象实例和其被反序列化后的对象实例,我们发现他们不是同一个对象,换句话说,在反序列化时会创建一个新的实例(即使定义构造函数为private)。

public class Example {
    public static void main(String[] args) {
        Singleton singleton = Singleton.getInstance();
        singleton.setValue(1); // Serialize
        try {
            FileOutputStream fileOut = new FileOutputStream("out.ser");
            ObjectOutputStream out = new ObjectOutputStream(fileOut);
            out.writeObject(singleton);
            out.close();
            fileOut.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        singleton.setValue(2); // Deserialize

        Singleton singleton2 = null;
        try {
            FileInputStream fileIn = new FileInputStream("out.ser");
            ObjectInputStream in = new ObjectInputStream(fileIn);
            singleton2 = (Singleton) in.readObject();
            in.close();
            fileIn.close();
        } catch (IOException i) {
            i.printStackTrace();
        } catch (ClassNotFoundException c) {
            System.out.println("singletons.SingletonEnum class not found");
            c.printStackTrace();
        }

        if (singleton == singleton2) {
            System.out.println("Two objects are same");
        } else {
            System.out.println("Two objects are not same");
        }
        System.out.println(singleton.getValue());
        System.out.println(singleton2.getValue());
    }
}

// 我们这里只是以 饿汉式 static final field的实现为例,但上面三种实现都有类似的问题
class Singleton implements java.io.Serializable  {
    //类加载时就初始化
    private static final Singleton instance = new Singleton();
    private int value;

    private Singleton() {
    }

    public static Singleton getInstance() {
        return instance;
    }

    public int getValue() {
        return value;
    }

    public void setValue(int i) {
        value = i;
    }
}

结果:

Two objects are not same
2
1

反射(reflection)

类似地,使用反射(reflection)可以强行调用私有构造器。

    public static void main(String[] args) throws Exception {
        Singleton singleton = Singleton.INSTANCE;
      
        Constructor constructor = singleton.getClass().getDeclaredConstructor(new Class[0]);
        constructor.setAccessible(true);
        Singleton singleton2 = (Singleton) constructor.newInstance();
      
        if (singleton == singleton2) {
            System.out.println("Two objects are same");
        } else {
            System.out.println("Two objects are not same");
        }
        singleton.setValue(1);
        singleton2.setValue(2);
        System.out.println(singleton.getValue());
        System.out.println(singleton2.getValue());
    }

结果:

Two objects are not same
1
2

解决

对于序列化时破坏单例模式的问题,我们虽然可以通过以下方式来避免:

//测试例子(四种写解决方式雷同)
public class Singleton implements java.io.Serializable {     
   public static Singleton INSTANCE = new Singleton();     

   protected Singleton() {     
   }  

   //反序列时直接返回当前INSTANCE
   private Object readResolve() {     
   		return INSTANCE;     
   }    
}   

因为,在反序列化过程中,如果被序列化的类中定义了readResolve 方法,虚拟机会试图调用对象类里的 readResolve 方法,以进行用户自定义的反序列化。

最终,实现了在序列化/反序列化过程中也不破坏单例模式。


类似地,对于反射时破坏单例模式的问题,我们虽然也可以通过以下方式来避免:

public static Singleton INSTANCE = new Singleton();     
private static volatile  boolean  flag = true;
private Singleton(){
    if(flag){
    		flag = false;   
    }else{
        throw new RuntimeException("The instance  already exists !");
    }
}

但是,问题确实也得到了解决,但这样的解决方案使得代码并不优美。

那有没有更简单更高效的呢?当然是有的,那就是枚举单例了。

枚举(Enum)实现单例模式

用枚举写单例实在太简单了!这也是它最大的优点。下面这段代码就是声明枚举实例的通常做法。

public enum SingletonEnum {
    INSTANCE;
    int value;

    public int getValue() {
        return value;
    }

    public void setValue(int value) {
        this.value = value;
    }
}

调用

public class EnumDemo {
    public static void main(String[] args) {
        SingletonEnum singleton = SingletonEnum.INSTANCE;
        System.out.println(singleton.getValue());
        singleton.setValue(2);
        System.out.println(singleton.getValue());
    }
}

我们可以通过EasySingleton.INSTANCE来访问实例,这比调用getInstance()方法简单多了。

由于默认枚举实例的创建是线程安全的,所以不需要担心线程安全的问题。但是在枚举中的其他任何方法的线程安全由程序员自己负责。还有防止上面的通过反射机制调用私用构造器。

这个版本基本上消除了绝大多数的问题。代码也非常简单,实在无法不用。这也是新版的《Effective Java》中推荐的模式。

分析

事实上,一个 enum类型是一个特殊的class类型。一个enum声明(declaration)会被编译成这样:

final class SingletonEnum extends Enum
{
		...

    public static final SingletonEnum INSTANCE;
    static 
    {
        INSTANCE = new SingletonEnum("INSTANCE", 0);
        $VALUES = (new SingletonEnum[] {
            INSTANCE
        });
    }
}

由于INSTANCE是final类型的,当你的代码第一次访问SingletonEnum.INSTANCE时,SingletonEnum类会被JVM加载并且初始化。

而由于Java类的加载和初始化过程都是线程安全的。所以,创建一个enum类型是线程安全的

优点

1 枚举写法简单

public enum EasySingleton{
    INSTANCE;
}

你可以通过EasySingleton.INSTANCE来访问。

2 枚举自己处理序列化

我们知道,以前的所有的单例模式都有一个比较大的问题,就是一旦实现了Serializable接口之后,就不再是单例的了,因为,每次调用 readObject()方法返回的都是一个新创建出来的对象。

有一种解决办法就是使用readResolve()方法来避免此事发生。

但是,**为了保证枚举类型像Java规范中所说的那样,每一个枚举类型及其定义的枚举变量在JVM中都是唯一的,在枚举类型的序列化和反序列化上,Java做了特殊的规定。**原文如下:

Enum constants are serialized differently than ordinary serializable or externalizable objects. The serialized form of an enum constant consists solely of its name; field values of the constant are not present in the form. To serialize an enum constant, ObjectOutputStream writes the value returned by the enum constant’s name method. To deserialize an enum constant, ObjectInputStream reads the constant name from the stream; the deserialized constant is then obtained by calling the java.lang.Enum.valueOf method, passing the constant’s enum type along with the received constant name as arguments. Like other serializable or externalizable objects, enum constants can function as the targets of back references appearing subsequently in the serialization stream. The process by which enum constants are serialized cannot be customized: any class-specific writeObject, readObject, readObjectNoData, writeReplace, and readResolve methods defined by enum types are ignored during serialization and deserialization. Similarly, any serialPersistentFields or serialVersionUID field declarations are also ignored–all enum types have a fixedserialVersionUID of 0L. Documenting serializable fields and data for enum types is unnecessary, since there is no variation in the type of data sent.

大概意思就是说,在序列化的时候,Java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过java.lang.Enum的valueOf方法来根据名字查找枚举对象。同时,编译器是不允许任何对这种序列化机制的定制,因此禁用了writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法。 我们看一下这个valueOf方法:

public static <T extends Enum<T>> T valueOf(Class<T> enumType,String name) {  
            T result = enumType.enumConstantDirectory().get(name);  
            if (result != null)  
                return result;  
            if (name == null)  
                throw new NullPointerException("Name is null");  
            throw new IllegalArgumentException(  
                "No enum const " + enumType +"." + name);  
        }  

从代码中可以看到,代码会尝试从调用enumType这个Class对象的enumConstantDirectory()方法返回的map中获取名字为name的枚举对象,如果不存在就会抛出异常。再进一步跟到enumConstantDirectory()方法,就会发现到最后会以反射的方式调用enumType这个类型的values()静态方法,也就是上面我们看到的编译器为我们创建的那个方法,然后用返回结果填充enumType这个Class对象中的enumConstantDirectory属性。

所以,JVM对序列化有保证。

3 枚举实例创建是线程安全的(thread-safe)

Reference