美文网首页
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、枚举类型线程安全

    1. 枚举线程安全 1.1 枚举简介   枚举是Java SE5 提供的一种新类型,关键字 enum 可以将一组具...

  • C++11 enum class

    传统的枚举类型不是类型安全的,会被视作整数。 C++11 枚举类型是类型安全的:不能够被隐式的转换为整数,同时也不...

  • 单例模式(Singleton)

    一、初始化单例类时即创建单例 饿汉式:(线程安全) 枚举类型:(线程安全) 二、按需、延迟创建单例 懒汉式:(线程...

  • java单例模式小结

    双检索实现的单例,是线程安全的。 枚举类型实现的单例,目前比较推荐

  • Java中枚举的线程安全性及序列化问题

    来源:微信公众号 ,原创:Hollis --枚举是如何保证线程安全的 要想看源码,首先得有一个类吧,那么枚举类型到...

  • Python入门与进阶(11-3)

    11-3 枚举类型、枚举名称与枚举值

  • 单例模式学习

    Ⅰ 懒汉式-线程不安全 Ⅱ 饿汉式-线程安全 Ⅳ 双重校验锁-线程安全 Ⅴ 静态内部类实现 Ⅵ 枚举实现 学习参考...

  • 单例模式

    懒汉(线程不安全) 懒汉(线程安全) 饿汉 饿汉(变种) 静态内部类 枚举 双重校验锁

  • C++11新特性

    强类型枚举 在C++11之前,枚举类型全局可见,两种枚举之间不可以叫相同名称。而C++11中不同枚举类型内部不会互...

  • 创建型模式 --- 单例

    1.懒汉式,线程不安全 2.懒汉式,线程安全 3.饿汉式,线程安全 4.枚举,线程安全 5.双检锁/双重校验锁

网友评论

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

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