美文网首页
11、枚举类型线程安全

11、枚举类型线程安全

作者: 火山_6c7b | 来源:发表于2020-08-03 22:18 被阅读0次

    1. 枚举线程安全

    1.1 枚举简介

      枚举是Java SE5 提供的一种新类型,关键字 enum 可以将一组具名的值的有限集合创建为一种新的类型,而这些具名的值可以作为常规的程序组件使用。

      但枚举类使用enum定义后在编译后默认继承了java.lang.Enum类,而不是普通的继承Object类。enum声明类继承了Serializable和Comparable两个接口。且采用enum声明后,该类会被编译器加上final声明(同String),故该类是无法继承的。枚举类的内部定义的枚举值就是该类的实例(且必须在第一行定义,当类初始化时,这些枚举值会被实例化)。

      由于这些枚举值的实例化是在类初始化阶段,所以应该将枚举类的构造器(如果存在),采用private声明(这种情况下默认也是private)。

    1.2 枚举类型解析

      枚举类型由enum声明,它和class一样是关键字,enum是无法被继承的。编译器会自动把枚举用继承Enum类来表示,但这一过程是由编译器完成的,枚举也不过是个语法糖。

    enum如何保证线程安全的呢,看下面的枚举示例:

    public enum Fruit {  
        APPLE, PEAR, PEACH, ORANGE;  
    }  
    

    Fruit是java.lang.Enum的子类,准确地说,是Enum<Fruit>的子类,这个继承是编译器帮我们做的,我们不能显式地去做。

    Fruit的反编译结果:

    public final class Fruit extends Enum  
    {  
        private Fruit(String s, int i)  
        {  
            super(s, i);  
        }  
      
        public static Fruit[] values()  
        {  
            Fruit afruit[];  
            int i;  
            Fruit afruit1[];  
            System.arraycopy(afruit = ENUM$VALUES, 0, afruit1 = new Fruit[i = afruit.length], 0, i);  
            return afruit1;  
        }  
      
        public static Fruit valueOf(String s)  
        {  
            return (Fruit)Enum.valueOf(test/Fruit, s);  
        }  
      
        public static final Fruit APPLE;  
        public static final Fruit PEAR;  
        public static final Fruit PEACH;  
        public static final Fruit ORANGE;  
        private static final Fruit ENUM$VALUES[];  
      
        static   
        {  
            APPLE = new Fruit("APPLE", 0);  
            PEAR = new Fruit("PEAR", 1);  
            PEACH = new Fruit("PEACH", 2);  
            ORANGE = new Fruit("ORANGE", 3);  
            ENUM$VALUES = (new Fruit[] {  
                APPLE, PEAR, PEACH, ORANGE  
            });  
        }  
    } 
    

      枚举类Fruit实际是继承了Enum的一个final类,该类和普通类无区别只是无法被继承。JDK Enum的实现也不过就是沿袭了Effective Java中提出的TypeSafeEnum模式,只不过是在编译器和JVM等更底层的级别上提供了支持。

    成员变量及初始化:

        public static final Fruit APPLE;  
        public static final Fruit PEAR;  
        public static final Fruit PEACH;  
        public static final Fruit ORANGE;  
        static   
        {  
            APPLE = new Fruit("APPLE", 0);  
            PEAR = new Fruit("PEAR", 1);  
            PEACH = new Fruit("PEACH", 2);  
            ORANGE = new Fruit("ORANGE", 3);  
            ENUM$VALUES = (new Fruit[] {  
                APPLE, PEAR, PEACH, ORANGE  
            });  
        } 
    

      成员变量都被声明为static final,表明其为类变量,且初始化语句放在了static代码块内,表明在类加载的准备阶段这些变量就会被初始化并赋值。

      当一个Java类第一次被真正使用到的时候静态资源被初始化、Java类的加载和初始化过程都是线程安全的。所以,创建一个enum类型是线程安全的。

    不能继续再从Fruit派生子类,那么哪来的多态呢?

    public enum Fruit {  
        APPLE {  
      
            public void test() {  
                System.out.println("I am an apple.");  
            }  
        },  
        PEAR {  
      
            public void test() {  
                System.out.println("I am a pear.");  
            }  
        },  
        PEACH {  
      
            public void test() {  
                System.out.println("I am a peach.");  
            }  
        },  
        ORANGE;  
      
        public void test() {  
            System.out.println("I am a fruit.");  
        }  
    }
    

      只有Orange没有Overide test()方法;

    调用示例:

    public static void main(String[] args) {  
            Fruit.APPLE.test();  
            Fruit.PEAR.test();  
            Fruit.PEACH.test();  
            Fruit.ORANGE.test();  
    }
    

    结果:

    I am an apple.
    I am a pear.
    I am a peach.
    I am a fruit.
    

      重新定义了test方法的APPLE,PEAR,PEACH覆盖了从父类继承过来的默认行为,而未从新定义test方法的ORANGE却沿袭了父类的行为,多态性在这里展现出来了。

      Fruit的反编译结果,没有任何新类继承自Fruit,那么这些多态行为是哪里冒出来的呢?说它是“多态”是否准确呢?其实,Fruit类在这个时候已经发生了微妙的变化,一切都与JDK的Enum的实现有关,我们现在可以到编译结果目录下面看看:

    Fruit class.png

      除了Fruit.class之外,还多了几个貌似是内部类的class文件,也许看到这里我们能有点线索了,不过还是看看反编译结果:

    import java.io.PrintStream;  
      
    public class Fruit extends Enum  
    {  
      
        private Fruit(String s, int i)  
        {  
            super(s, i);  
        }  
      
        public void test()  
        {  
            System.out.println("I am a fruit.");  
        }  
      
        public static Fruit[] values()  
        {  
            Fruit afruit[];  
            int i;  
            Fruit afruit1[];  
            System.arraycopy(afruit = ENUM$VALUES, 0, afruit1 = new Fruit[i = afruit.length], 0, i);  
            return afruit1;  
        }  
      
        public static Fruit valueOf(String s)  
        {  
            return (Fruit)Enum.valueOf(test/Fruit, s);  
        }  
      
        Fruit(String s, int i, Fruit fruit)  
        {  
            this(s, i);  
        }  
      
        public static final Fruit APPLE;  
        public static final Fruit PEAR;  
        public static final Fruit PEACH;  
        public static final Fruit ORANGE;  
        private static final Fruit ENUM$VALUES[];  
      
        static   
        {  
            APPLE = new Fruit("APPLE", 0) {  
      
                public void test()  
                {  
                    System.out.println("I am an apple.");  
                }  
      
            };  
            PEAR = new Fruit("PEAR", 1) {  
      
                public void test()  
                {  
                    System.out.println("I am a pear.");  
                }  
      
            };  
            PEACH = new Fruit("PEACH", 2) {  
      
                public void test()  
                {  
                    System.out.println("I am a peach.");  
                }  
      
            };  
            ORANGE = new Fruit("ORANGE", 3);  
            ENUM$VALUES = (new Fruit[] {  
                APPLE, PEAR, PEACH, ORANGE  
            });  
        }  
    }  
    

    注意:

        static   
        {  
            APPLE = new Fruit("APPLE", 0) {  
      
                public void test()  
                {  
                    System.out.println("I am an apple.");  
                }  
      
            };  
            PEAR = new Fruit("PEAR", 1) {  
      
                public void test()  
                {  
                    System.out.println("I am a pear.");  
                }  
      
            };  
            PEACH = new Fruit("PEACH", 2) {  
      
                public void test()  
                {  
                    System.out.println("I am a peach.");  
                }  
      
            };  
            ORANGE = new Fruit("ORANGE", 3);  
            ENUM$VALUES = (new Fruit[] {  
                APPLE, PEAR, PEACH, ORANGE  
            });  
        }
    

      这个时候的APPLE,PEAR,PEACH已经以匿名内部类的方式对Fruit进行了Overide,自然体现出了多态,多出的那三个疑似内部类的class文件也就是它们!而ORANGE,没有重写test方法,仍然以一个Fruit实例的形式出现。

    1.3 枚举类型的线程安全及单例

      前面说了类加载的准备阶段枚举就会被初始化并赋值,而Java类的加载和初始化过程都是线程安全的,由jvm保证。因为ClassLoader进行类加载时,加了同步锁synchronized。

    ClassLoader.loadClass()源码:

        protected Class<?> loadClass(String name, boolean resolve)
            throws ClassNotFoundException
        {
            synchronized (getClassLoadingLock(name)) {
                // First, check if the class has already been loaded
                Class<?> c = findLoadedClass(name);
                if (c == null) {
                    long t0 = System.nanoTime();
                    try {
                        if (parent != null) {
                            c = parent.loadClass(name, false);
                        } else {
                            c = findBootstrapClassOrNull(name);
                        }
                    } catch (ClassNotFoundException e) {
                        // ClassNotFoundException thrown if class not found
                        // from the non-null parent class loader
                    }
     
                    if (c == null) {
                        // If still not found, then invoke findClass in order
                        // to find the class.
                        long t1 = System.nanoTime();
                        c = findClass(name);
     
                        // this is the defining class loader; record the stats
                        sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                        sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                        sun.misc.PerfCounter.getFindClasses().increment();
                    }
                }
                if (resolve) {
                    resolveClass(c);
                }
                return c;
            }
        }
    

    1.4 枚举类型的序列化及反序列化

      所有的单例模式都有一个比较大的问题,就是一旦实现了Serializable接口之后,就不再是单例得了,因为,每次调用 readObject()方法返回的都是一个新创建出来的对象,有一种解决办法就是使用readResolve()方法来避免此事发生。但是,为了保证枚举类型像Java规范中所说的那样,每一个枚举类型极其定义的枚举变量在JVM中都是唯一的,在枚举类型的序列化和反序列化上,Java做了特殊的规定。

      枚举类型在序列化的时候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对序列化有保证。

      普通的Java类的反序列化过程中,会通过反射调用类的默认构造函数来初始化对象。所以,即使单例中构造函数是私有的,也会被反射给破坏掉。由于反序列化后的对象是重新new出来的,所以这就破坏了单例。(使用双重校验锁实现的单例其实是存在一定问题的,就是这种单例有可能被序列化锁破坏)

      普通类的反序列化是通过反射实现的,枚举类的反序列化不是通过反射实现的。所以,枚举类也就不会发生由于反序列化导致的单例破坏问题。

    相关文章

      网友评论

          本文标题:11、枚举类型线程安全

          本文链接:https://www.haomeiwen.com/subject/itxdrktx.html