当前位置: 代码迷 >> 综合 >> 枚举实现单例那些事儿
  详细解决方案

枚举实现单例那些事儿

热度:82   发布时间:2023-10-23 16:56:53.0

《Effective Java》一书中提到,单元素的枚举类型,功能完整、使用简洁、无偿提供了序列化机制,在面对复杂的序列化或者反射攻击时仍然可以绝对防止多次实例化等优点,被作者认为是实现单例模式的最佳方法(也是一种饿汉式)。

实现非常简单:

public enum Singleton {  INSTANCE;  public void getOtherMethod() {  }  
} 

调用也非常简单:

Singleton.INSTANCE.getOtherMethod();

那么,枚举是如何保证线程安全的呢?首先我们来看一个简单的枚举类。

public enum DemoEnum {FIRST,SECOND,THIRD,FOURTH
}

然后将它反编译,看看这段代码是如何实现的

public final class DemoEnum extends Enum {private DemoEnum(String s, int i) {super(s, i);}public static DemoEnum[] values() {DemoEnum at[];int i;DemoEnum at1[];System.arraycopy(at = ENUM$VALUES, 0, at1 = new T[i = at.length], 0, i);return at1;}public static DemoEnum valueOf(String s) {return (DemoEnum)Enum.valueOf(demo/DemoEnum, s);}public static final DemoEnum FIRST;public static final DemoEnum SECOND;public static final DemoEnum THIRD;public static final DemoEnum FOURTH;private static final DemoEnum ENUM$VALUES[];static {SPRING = new T("FIRST", 0);SUMMER = new T("SECOND", 1);AUTUMN = new T("THIRD", 2);WINTER = new T("FOURTH", 3);ENUM$VALUES = (new T[] {FIRST, SECOND, THIRD, FOURTH});}
}

我们可以看到,这个类是final的,不可被继承,并且继承了Enum。并且这个类中的属性和方法都是static的,在类被加载之后初始化。虚拟机在加载类的时候,使用了ClassLoader的loadClass方法,这个方法使用了同步代码块保证了线程安全。

序列化对单例的破坏(非枚举)

序列化会通过反射调用无参数的构造方法创建一个新的对象

下面我们来具体解释一下,

写个测试类:

public class Singleton implements Serializable{private final static Singleton INSTANCE  = new Singleton();private Singleton(){};public static Singleton getInstance(){return INSTANCE;}public static void main(String[] args) throws FileNotFoundException, IOException, ClassNotFoundException {Singleton singleton1 = Singleton.getInstance();File file= new File("/singleton");ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(file));out.writeObject(singleton1);out.close();ObjectInputStream in = new ObjectInputStream(new FileInputStream(file));Singleton singleton2 = (Singleton) in.readObject();in.close();System.out.println(singleton1 == singleton2);}
}

输出结果为false

说明对单例进行序列化和反序列化得到的是两个对象,破坏了单例性。

对象的序列化过程通过ObjectOutputStream和ObjectInputStream实现,ObjectInputStream中的readObject方法代码如下:

public final Object readObject() throws IOException, ClassNotFoundException {if (enableOverride) {return readObjectOverride();}// if nested read, passHandle contains handle of enclosing objectint outerHandle = passHandle;try {Object obj = readObject0(false);handles.markDependency(outerHandle, passHandle);ClassNotFoundException ex = handles.lookupException(passHandle);if (ex != null) {throw ex;}if (depth == 0) {vlist.doCallbacks();}return obj;} finally {passHandle = outerHandle;if (closed && depth == 0) {clear();}}}
可以看到,调用了readObject0方法,这个方法中new一个Object对象,调用了readOrdinaryObject方法,我们来具体看下:
private Object readOrdinaryObject(boolean unshared)throws IOException{if (bin.readByte() != TC_OBJECT) {throw new InternalError();}ObjectStreamClass desc = readClassDesc(false);desc.checkDeserialize();Class<?> cl = desc.forClass();if (cl == String.class || cl == Class.class|| cl == ObjectStreamClass.class) {throw new InvalidClassException("invalid class descriptor");}Object obj;try {obj = desc.isInstantiable() ? desc.newInstance() : null;} catch (Exception ex) {throw (IOException) new InvalidClassException(desc.forClass().getName(),"unable to create instance").initCause(ex);}passHandle = handles.assign(unshared ? unsharedMarker : obj);ClassNotFoundException resolveEx = desc.getResolveException();if (resolveEx != null) {handles.markException(passHandle, resolveEx);}if (desc.isExternalizable()) {readExternalData((Externalizable) obj, desc);} else {readSerialData(obj, desc);}handles.finish(passHandle);if (obj != null &&handles.lookupException(passHandle) == null &&desc.hasReadResolveMethod()){Object rep = desc.invokeReadResolve(obj);if (unshared && rep.getClass().isArray()) {rep = cloneArray(rep);}if (rep != obj) {handles.setObject(passHandle, obj = rep);}}return obj;}

isInstantiable :如果一个serializable/externalizable的类可以在运行时被实例化,这个方法就返回true.

如果返回false,就通过反射调用无参构造方法新建一个对象。

那么,我们怎么防止序列化破环单例模式呢?

在单例类中定义readResolve

private Object readResolve() {return INSTANCE;
}

再运行上面的测试类,输出结果为true,这是为什么呢?

在上面的readOrdinaryObject方法中,会判断hasReadResolveMethod,如果存在readResolve方法,返回true,就调用要被反序列化的类的readResolve方法

反射对单例的破坏

测试类:

public class Singleton{private final static Singleton INSTANCE  = new Singleton();private Singleton(){};public static Singleton getInstance(){return INSTANCE;}public static void main(String[] args) throws FileNotFoundException, IOException, ClassNotFoundException, SecurityException, NoSuchMethodException, IllegalArgumentException, InstantiationException, IllegalAccessException, InvocationTargetException {Singleton singleton1 = Singleton.getInstance();Constructor<Singleton> c = Singleton.class.getDeclaredConstructor();c.setAccessible(true);Singleton singleton2 = c.newInstance();System.out.println(singleton1 == singleton2);}
}

输出结果为false,说明使用反射调用私有构造器会破坏单例

解决办法:在构造方法中增加校验

public class Singleton{private final static Singleton INSTANCE  = new Singleton();private static int COUNT = 0;private Singleton(){if(++COUNT > 1){throw new RuntimeException("can not be construt more than once");}};public static Singleton getInstance(){return INSTANCE;}
}

当使用反射调用时,就会抛个异常。

克隆对单例的破坏

测试类:

public class Singleton implements Cloneable{private final static Singleton INSTANCE  = new Singleton();public static Singleton getInstance(){return INSTANCE;}public static void main(String[] args) {Singleton singleton1 = Singleton.getInstance();Singleton singleton2 = (Singleton) sigleton1.clone();System.out.println(singleton1 == singleton2);}

输出结果为false

解决办法:重写clone方法

protected Object clone() throws CloneNotSupportedException {return INSTANCE;
}

如果想要避免上述问题,最简单的办法就是使用枚举

1)反序列化会返回instance对象

2)反射会抛异常:java.lang.NoSuchMethodException

3)没有clone方法

下面详细介绍一下,枚举是如何解决反序列化破坏单例的问题

枚举怎样解决反序列化破坏单例?

序列化时Java仅将枚举对象的name属性输出到结果中,反序列化时通过java.lang.Enum的valueOf方法来根据name查找枚举对象。同时,编译器不允许任何对这种序列化机制的定制,因此禁用了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);  
}  

enumConstantDirectory()得到的返回值是个map,用传入的name匹配对应的枚举对象,不存在就抛个异常。

Map<String, T> enumConstantDirectory() {if (enumConstantDirectory == null) {T[] universe = getEnumConstantsShared();if (universe == null)throw new IllegalArgumentException(getName() + " is not an enum type");Map<String, T> m = new HashMap<>(2 * universe.length);for (T constant : universe)m.put(((Enum<?>)constant).name(), constant);enumConstantDirectory = m;}return enumConstantDirectory;}

我们可以看到,最后是用反射得到这个类型的values方法

T[] getEnumConstantsShared() {if (enumConstants == null) {if (!isEnum()) return null;try {final Method values = getMethod("values");java.security.AccessController.doPrivileged(new java.security.PrivilegedAction<Void>() {public Void run() {values.setAccessible(true);return null;}});enumConstants = (T[])values.invoke(null);}// These can happen when users concoct enum-like classes// that don't comply with the enum spec.catch (InvocationTargetException ex) { return null; }catch (NoSuchMethodException ex) { return null; }catch (IllegalAccessException ex) { return null; }}return enumConstants;}

综上所述,枚举可以作为构建单例的最佳选择。

以上。

To be continued...

  相关解决方案