Java中枚舉的線程安全性及序列化問題

Java SE5提供了一種新的類型-Java的枚舉類型,關鍵字enum可以將一組具名的值的有限集合創建爲一種新的類型,而這些具名的值可以作爲常規的程序組件使用,這是一種非常有用的功能。本文將深入分析枚舉的源碼,看一看枚舉是怎麼實現的,他是如何保證線程安全的,以及爲什麼用枚舉實現的單例是最好的方式

枚舉是如何保證線程安全的

要想看源碼,首先得有一個類吧,那麼枚舉類型到底是什麼類呢?是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關鍵字告訴我們,這個類也是不能被繼承的。當我們使用enmu來定義一個枚舉類型的時候,編譯器會自動幫我們創建一個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做了特殊的規定。英文原文我就不貼了。

 

大概意思就是說,在序列化的時候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對序列化有保證。

3.枚舉實例創建是thread-safe(線程安全的)

我們在深度分析Java的ClassLoader機制(源碼級別)和Java類的加載、鏈接和初始化兩個文章中分別介紹過,當一個Java類第一次被真正使用到的時候靜態資源被初始化、Java類的加載和初始化過程都是線程安全的。所以,創建一個enum類型是線程安全的。

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章