java枚举的线程安全及序列化

原文链接:https://www.cnblogs.com/z00377750/p/9177097.html

https://www.cnblogs.com/chiclee/p/9097772.html

枚举是如何保证线程安全的

要想看源码,首先得有一个类吧,那么枚举类型到底是什么类呢?是enum吗?答案很明显不是,enum就和class一样,只是一个关键字,他并不是一个类,那么枚举是由什么类维护的呢,我们简单的写一个枚举:

public enum t {
    SPRING,SUMMER,AUTUMN,WINTER;
}

然后我们使用反编译,看看这段代码到底是怎么实现的,反编译(Java的反编译)后代码内容如下:

public final class T extends Enum
{
    private T(String s, int i)
    {
        super(s, i);
    }
    public static T[] values()
    {
        T at[];
        int i;
        T at1[];
        System.arraycopy(at = ENUM$VALUES, 0, at1 = new T[i = at.length], 0, i);
        return at1;
    }

    public static T valueOf(String s)
    {
        return (T)Enum.valueOf(demo/T, s);
    }

    public static final T SPRING;
    public static final T SUMMER;
    public static final T AUTUMN;
    public static final T WINTER;
    private static final T ENUM$VALUES[];
    static
    {
        SPRING = new T("SPRING", 0);
        SUMMER = new T("SUMMER", 1);
        AUTUMN = new T("AUTUMN", 2);
        WINTER = new T("WINTER", 3);
        ENUM$VALUES = (new T[] {
            SPRING, SUMMER, AUTUMN, WINTER
        });
    }
}

通过反编译后代码我们可以看到,public final class T extends Enum,说明,该类是继承了Enum类的,同时final关键字告诉我们,这个类也是不能被继承的。当我们使用enum来定义一个枚举类型的时候,编译器会自动帮我们创建一个final类型的类继承Enum类,所以枚举类型不能被继承,我们看到这个类中有几个属性和方法。

我们可以看到:

public static final T SPRING;
public static final T SUMMER;
public static final T AUTUMN;
public static final T WINTER;
private static final T ENUM$VALUES[];
static
{
    SPRING = new T("SPRING", 0);
    SUMMER = new T("SUMMER", 1);
    AUTUMN = new T("AUTUMN", 2);
    WINTER = new T("WINTER", 3);
    ENUM$VALUES = (new T[] {
        SPRING, SUMMER, AUTUMN, WINTER
    });
}

都是static类型的,因为static类型的属性会在类被加载之后被初始化,我们在深度分析Java的ClassLoader机制(源码级别)Java类的加载、链接和初始化两个文章中分别介绍过,当一个Java类第一次被真正使用到的时候静态资源被初始化、Java类的加载和初始化过程都是线程安全的。所以,创建一个enum类型是线程安全的。

为什么用枚举实现的单例是最好的方式

【设计模式】【创造型模式】单例模式中,我们看到一共有六种实现单例的方式,其中,Effective Java,作者,Josh Bloch, 提倡使用枚举的方式,既然大神说这种方式好,那我们就要知道它为什么好?

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.EnumvalueOf方法来根据名字查找枚举对象。同时,编译器是不允许任何对这种序列化机制的定制的,因此禁用了writeObject、readObject、readObjectNoData、writeReplacereadResolve等方法。 我们看一下这个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(线程安全的)

我们在深度分析Java的ClassLoader机制(源码级别)Java类的加载、链接和初始化两个文章中分别介绍过,当一个Java类第一次被真正使用到的时候静态资源被初始化、Java类的加载和初始化过程都是线程安全的。所以,创建一个enum类型是线程安全的。

4.避免反射攻击

 枚举Enum是个抽象类,其实一旦一个类声明为枚举,实际上就是继承了Enum,所以会有(String.class,int.class)的构造器。既然是可以获取到父类Enum的构造器,那你也许会说刚才我的反射是因为自身的类没有无参构造方法才导致的异常,并不能说单例枚举避免了反射攻击。好的,那我们就使用父类Enum的构造器,看看是什么情况:

 
 1 public enum  EnumSingleton {
 2     INSTANCE;
 3     public EnumSingleton getInstance(){
 4         return INSTANCE;
 5     }
 6 
 7     public static void main(String[] args) throws IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException {
 8         EnumSingleton singleton1=EnumSingleton.INSTANCE;
 9         EnumSingleton singleton2=EnumSingleton.INSTANCE;
10         System.out.println("正常情况下,实例化两个实例是否相同:"+(singleton1==singleton2));
11         Constructor<EnumSingleton> constructor= null;
12 //        constructor = EnumSingleton.class.getDeclaredConstructor();
13         constructor = EnumSingleton.class.getDeclaredConstructor(String.class,int.class);//其父类的构造器
14         constructor.setAccessible(true);
15         EnumSingleton singleton3= null;
16         //singleton3 = constructor.newInstance();
17         singleton3 = constructor.newInstance("testInstance",66);
18         System.out.println(singleton1+"\n"+singleton2+"\n"+singleton3);
19         System.out.println("通过反射攻击单例模式情况下,实例化两个实例是否相同:"+(singleton1==singleton3));
20     }
21 }
 

然后咱们看运行结果:

 
正常情况下,实例化两个实例是否相同:true
Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
    at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
    at com.lxp.pattern.singleton.EnumSingleton.main(EnumSingleton.java:25)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at com.intellij.rt.execution.application.AppMain.main(AppMain.java:144)
 

        继续报异常。之前是因为没有无参构造器,这次拿到了父类的构造器了,只是在执行第17行(我没有复制import等包,所以行号少于我自己运行的代码)时候抛出异常,说是不能够反射,Constructor类的newInstance方法源码:

 1 @CallerSensitive
 2     public T newInstance(Object ... initargs)
 3         throws InstantiationException, IllegalAccessException,
 4                IllegalArgumentException, InvocationTargetException
 5     {
 6         if (!override) {
 7             if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
 8                 Class<?> caller = Reflection.getCallerClass();
 9                 checkAccess(caller, clazz, null, modifiers);
10             }
11         }
12         if ((clazz.getModifiers() & Modifier.ENUM) != 0)
13             throw new IllegalArgumentException("Cannot reflectively create enum objects");
14         ConstructorAccessor ca = constructorAccessor;   // read volatile
15         if (ca == null) {
16             ca = acquireConstructorAccessor();
17         }
18         @SuppressWarnings("unchecked")
19         T inst = (T) ca.newInstance(initargs);
20         return inst;
21     }

请看黄颜色标注的第12行源码,说明反射在通过newInstance创建对象时,会检查该类是否ENUM修饰,如果是则抛出异常,反射失败。

 5.flag防反射

我么都知道单例模式是为了实现只创建一个实例,但是java 的反射机制可以破坏这种单例模式,这种通过反射来破坏单例的例子网上多的是,我就不列出来了,今天我们来说说怎么防止使用java的反射机制来破坏单例,查了查网上有很多是在单例的类中增加一个boolean类型的flag,然后在构造函数中来改变flag的值,然后再调用构造函数的时候判断这个flag是否已经改变了,这样来防止重复创建对象实例,当看到这种解决办法的时候就觉得这种方法是不严谨的,因为既然可以通过反射来获取构造函数来创建实例了,那么同样可以通过反射来获取到你定义的flag,那么在利用反射调用构造函数之前,先获取到你这个flag,然后将值重置,那么再次调用构造函数就不会受到限制了,那这样实际上就没有起到防止重复创建的效果。于是我使用了自己想出来的如下方法:

public class SingletonClass {
// private static final Boolean flag = Boolean.FALSE;
private static ImmutableBoolean flag = new ImmutableBoolean();
private SingletonClass() {
if (flag.getCount() <= 0) {
synchronized (SingletonClass.class) {
if (flag.getCount() <= 0) {
flag.setCount();
} else {
throw new RuntimeException("正在破坏单例模式1");
}
}
} else {
throw new RuntimeException("正在破坏单例模式2");
}
}
private static SingletonClass singletonClass = null;

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

private static class ImmutableBoolean {
private static int count = 0;
public ImmutableBoolean() {

}

public void setCount() {
synchronized (SingletonClass.class) {
if (count <= 0) {
count ++;
} else {
throw new RuntimeException("");
}
}
}

public int getCount(){
return count;
}
}
}

 

public class TestClass {
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
SingletonClass s1 = SingletonClass.getInstance();
// System.out.println(s1);
Class<SingletonClass> c1 = SingletonClass.class;
// s1.new ImmutableBoolean();
// SingletonClass.ImmutableBoolean
// Field[] fields = c1.getDeclaredFields();
// for (Field field : fields) {
// System.out.println(field.getName());
// if ("flag".equals(field.getName())) {
// field.setAccessible(true);
// Class c2 = field.getClass();
// System.out.println(c2.getName());
//// Class c3 = field.
//// System.out.println(c3.getName());
//// field.
//// field.setBoolean(s1, false);
//// break;
// }
// }

Constructor<SingletonClass> constructor = c1.getDeclaredConstructor();
constructor.setAccessible(true);
SingletonClass s2 = constructor.newInstance();
System.out.println(s1 == s2);
}
}

 


我定义了一个私有的内部类,然后用这个内部类的属性来作为flag,这样外面的其他类就获取不到这个私有的内部类,也就不能改变它的值了,从而保护了单例的实现。

posted @ 2019-07-30 22:42  枫树湾河桥  阅读(1064)  评论(0编辑  收藏  举报
Live2D