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出来的,所以这就破坏了单例。(使用双重校验锁实现的单例其实是存在一定问题的,就是这种单例有可能被序列化锁破坏)
普通类的反序列化是通过反射实现的,枚举类的反序列化不是通过反射实现的。所以,枚举类也就不会发生由于反序列化导致的单例破坏问题。
网友评论