深刻理解單例模式

ObjectInputStream緩存

對象的序列化過程經過ObjectOutputStream和ObjectInputputStream來實現的,那麼帶着剛剛的問題,分析一下ObjectInputputStream的readObject 方法執行狀況究竟是怎樣的。安全

爲了節省篇幅,這裏給出ObjectInputStream的readObject的調用棧:app

你們順着此圖的關係,去看readObject方法的實現。
首先進入readObject0方法裏,關鍵代碼以下:ide

1ui

2spa

3線程

4code

5對象

6繼承

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

switch (tc) {

    //省略部分代碼

 

    case TC_STRING:

    case TC_LONGSTRING:

        return checkResolve(readString(unshared));

 

    case TC_ARRAY:

        return checkResolve(readArray(unshared));

 

    case TC_ENUM:

        return checkResolve(readEnum(unshared));

 

    case TC_OBJECT:

        return checkResolve(readOrdinaryObject(unshared));

 

    case TC_EXCEPTION:

        IOException ex = readFatalException();

        throw new WriteAbortedException("writing aborted", ex);

 

    case TC_BLOCKDATA:

    case TC_BLOCKDATALONG:

        if (oldMode) {

            bin.setBlockDataMode(true);

            bin.peek();             // force header read

            throw new OptionalDataException(

                bin.currentBlockRemaining());

        } else {

            throw new StreamCorruptedException(

                "unexpected block data");

        }

 

    //省略部分代碼

這裏就是判斷目標對象的類型,不一樣類型執行不一樣的動做。咱們的是個普通的Object對象,天然就是進入case TC_OBJECT的代碼塊中。而後進入readOrdinaryObject方法中。
readOrdinaryObject方法的代碼片斷:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

private Object readOrdinaryObject(boolean unshared)

        throws IOException {

    //此處省略部分代碼

 

    Object obj;

    try {

        obj = desc.isInstantiable() ? desc.newInstance() : null;

    } catch (Exception ex) {

        throw (IOException) new InvalidClassException(

            desc.forClass().getName(),

            "unable to create instance").initCause(ex);

    }

 

    //此處省略部分代碼

 

    if (obj != null &&

        handles.lookupException(passHandle) == null &&

        desc.hasReadResolveMethod())

    {

        Object rep = desc.invokeReadResolve(obj);

        if (unshared && rep.getClass().isArray()) {

            rep = cloneArray(rep);

        }

        if (rep != obj) {

            handles.setObject(passHandle, obj = rep);

        }

    }

 

    return obj;

}

重點看代碼塊:

1

2

3

4

5

6

7

8

Object obj;

try {

     obj = desc.isInstantiable() ? desc.newInstance() : null;

 } catch (Exception ex) {

     throw (IOException) new InvalidClassException(

         desc.forClass().getName(),

         "unable to create instance").initCause(ex);

 }

這裏建立的這個obj對象,就是本方法要返回的對象,也能夠暫時理解爲是ObjectInputStream的readObject返回的對象。

isInstantiable:若是一個serializable/externalizable的類能夠在運行時被實例化,那麼該方法就返回true。針對serializable和externalizable我會在其餘文章中介紹。 
desc.newInstance:該方法經過反射的方式調用無參構造方法新建一個對象。

因此。到目前爲止,也就能夠解釋,爲何序列化能夠破壞單例了?即序列化會經過反射調用無參數的構造方法建立一個新的對象

接下來再看,爲何在單例類中定義readResolve就能夠解決該問題呢?仍是在readOrdinaryObjec方法裏繼續往下看。

1

2

3

4

5

6

7

8

9

10

11

12

if (obj != null &&

            handles.lookupException(passHandle) == null &&

            desc.hasReadResolveMethod())

{

    Object rep = desc.invokeReadResolve(obj);

     if (unshared && rep.getClass().isArray()) {

         rep = cloneArray(rep);

     }

     if (rep != obj) {

         handles.setObject(passHandle, obj = rep);

     }

}

這段代碼也很清楚地給出答案了!
若是目標類有readResolve方法,那就經過反射的方式調用要被反序列化的類的readResolve方法,返回一個對象,而後把這個新的對象複製給以前建立的obj(即最終返回的對象)。那readResolve 方法裏是什麼?就是直接返回咱們的單例對象。

1

2

3

4

5

6

7

8

9

10

11

public class Elvis implements Serializable {

    public static final Elvis INSTANCE = new Elvis();

 

    private Elvis() {

        System.err.println("Elvis Constructor is invoked!");

    }

 

    private Object readResolve() {

       return INSTANCE;

    }

}

因此,原理也就清楚了,主要在Singleton中定義readResolve方法,並在該方法中指定要返回的對象的生成策略,就能夠防止單例被破壞。

單元素枚舉類型

第三種實現單例的方式是,聲明一個單元素的枚舉類:

1

2

3

4

5

// Enum singleton - the preferred approach

public enum Elvis {

    INSTANCE;

    public void leaveTheBuilding() { ... }

}

這個方法跟提供公有的字段方法很相似,但它更簡潔,提供自然的可序列化機制和可以強有力地保證不會出現屢次實例化的狀況 ,甚至面對複雜的序列化和反射的攻擊下。這種方法可能看起來不太天然,可是擁有單元素的枚舉類型多是實現單例模式的最佳實踐。注意,若是單例必需要繼承一個父類而非枚舉的狀況下是沒法使用該方式的(不過能夠聲明一個實現了接口的枚舉)。
咱們分析一下,枚舉類型是如何阻止反射來建立實例的?直接源碼:
看Constructor類的newInstance方法。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

public T newInstance(Object ... initargs)

        throws InstantiationException, IllegalAccessException,

               IllegalArgumentException, InvocationTargetException

{

    if (!override) {

        if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {

            Class<?> caller = Reflection.getCallerClass();

            checkAccess(caller, clazz, null, modifiers);

        }

    }

    if ((clazz.getModifiers() & Modifier.ENUM) != 0)

        throw new IllegalArgumentException("Cannot reflectively create enum objects");

    ConstructorAccessor ca = constructorAccessor;   // read volatile

    if (ca == null) {

        ca = acquireConstructorAccessor();

    }

    @SuppressWarnings("unchecked")

    T inst = (T) ca.newInstance(initargs);

    return inst;

}

這行代碼(clazz.getModifiers() & Modifier.ENUM) != 0 就是用來判斷目標類是否是枚舉類型,若是是拋出異常IllegalArgumentException("Cannot reflectively create enum objects"),沒法經過反射建立枚舉對象!很顯然,反射無效了。

接下來,再看一下反序列化是如何預防的。依然按照上面說的順序去找到枚舉類型對應的readEnum方法,以下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

private Enum<?> readEnum(boolean unshared) throws IOException {

    if (bin.readByte() != TC_ENUM) {

        throw new InternalError();

    }

 

    ObjectStreamClass desc = readClassDesc(false);

    if (!desc.isEnum()) {

        throw new InvalidClassException("non-enum class: " + desc);

    }

 

    int enumHandle = handles.assign(unshared ? unsharedMarker : null);

    ClassNotFoundException resolveEx = desc.getResolveException();

    if (resolveEx != null) {

        handles.markException(enumHandle, resolveEx);

    }

 

    String name = readString(false);

    Enum<?> result = null;

    Class<?> cl = desc.forClass();

    if (cl != null) {

        try {

            @SuppressWarnings("unchecked")

            Enum<?> en = Enum.valueOf((Class)cl, name);

            result = en;

        } catch (IllegalArgumentException ex) {

            throw (IOException) new InvalidObjectException(

                "enum constant " + name + " does not exist in " +

                cl).initCause(ex);

        }

        if (!unshared) {

            handles.setObject(enumHandle, result);

        }

    }

 

    handles.finish(enumHandle);

    passHandle = enumHandle;

    return result;

}

readString(false):首先獲取到枚舉對象的名稱name。
Enum<?> en = Enum.valueOf((Class)cl, name):再指定名稱的指定枚舉類型得到枚舉常量,因爲枚舉中的name是惟一,切對應一個枚舉常量。因此咱們獲取到了惟一的常量對象。這樣就沒有建立新的對象,維護了單例屬性。

看看Enum.valueOf 的JavaDoc文檔:

  • 返回具備指定名稱的指定枚舉類型的枚舉常量。 該名稱必須與用於聲明此類型中的枚舉常量的標識符徹底匹配。 (不容許使用無關的空白字符。)

具體實現:

1

2

3

4

5

6

7

8

9

10

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 constant " + enumType.getCanonicalName() + "." + name);

    }

enumConstantDirectory():返回一個Map,維護着名稱到枚舉常量的映射。咱們就是從這個Map裏獲取已經聲明的枚舉常量,經過這個緩存池同樣的組件,讓咱們能夠重用這個枚舉常量!

總結

  1. 常見的單例寫法有他的弊端,存在安全性問題,如:反射,序列化的影響。
  2. 《Effective Java》做者Josh Bloch 提倡使用單元素枚舉類型的方式來實現單例,首先建立一個枚舉很簡單,其次枚舉常量是線程安全的,最後有自然的可序列化機制和防反射的機制。
相關文章
相關標籤/搜索