首頁>技術>

前言1.單例是什麼?

單例模式:是一種建立型設計模式,目的是保證全域性一個類只有一個例項物件,分為懶漢式和餓漢式。所謂懶漢式,類似於懶載入,需要的時候才會觸發初始化例項物件。而餓漢式正好相反,專案啟動,類載入的時候,就會建立初始化單例物件。

1.1 優點

如果只有一個例項,那麼就可以少佔用系統資源,節省記憶體,訪問也會相對較快。比較靈活。

1.2 缺點

不能使用在變化的物件上,特別是不同請求會造成不同屬性的物件。由於Spring本身預設例項就是單例的,所以使用的時候需要判斷應用場景,要不會造成張冠李戴的現象。而往往操作引用和集合,就更不容易查詢到這種詭異的問題。例如:一些配置獲取,如果後期使用需要修改其值,要麼定義使用單例,後期使用深複製,要麼不要使用單例。

既然使用單例模式,那麼就得想盡一切辦法,保證例項是唯一的,這也是單例模式的使命。但是程式碼是人寫的,再完美的人也可能寫出不那麼完美的程式碼,再安全的系統,也有可能存在漏洞。既然你想保證單例,那我偏偏找出方法,建立同一個類多個不同的物件呢?這就是對單例模式的破壞,到底有哪些方式可以破壞單例模式呢?主要但是不限於以下幾種:

沒有將構造器私有化,可以直接呼叫。反射呼叫構造器實現了cloneable介面序列化與反序列化2. 破壞單例的幾種方法2.1 透過構造器建立物件

一般來說,一個稍微 ✔️ 的單例模式,是不可以透過new來建立物件的,這個嚴格意義上不屬於單例模式的破壞。但是人不是完美的,寫出的程式也不可能是完美的,總會有時候疏忽了,忘記了將構造器私有化,那麼外部就可以直接呼叫到構造器,自然就可以破壞單例模式,所以這種寫法就是不成功的單例模式。

/** * 下面是使用雙重校驗鎖方式實現單例 */public class Singleton{    private volatile static Singleton singleton;    public static Singleton getSingleton() {        if (singleton == null) {            synchronized (Singleton.class) {                if (singleton == null) {                    singleton = new Singleton();                }            }        }        return singleton;    }}

上面就是使用雙重檢察鎖的方式,實現單例模式,但是忘記了寫private的構造器,預設是有一個public的構造器,如果呼叫會怎麼樣呢?

    public static void main(String[] args) {        Singleton singleton = new Singleton();        Singleton singleton1 = new Singleton();        System.out.println(singleton.hashCode());        System.out.println(singleton1.hashCode());        System.out.println(Singleton.getSingleton().hashCode());    }

執行的結果如下:

69240403615548745021846274136

三個物件的hashcode都不一樣,所以它們不是同一個物件,這樣也就證明了,這種單例寫法是不成功的。

2.2 反射呼叫構造器

如果單例類已經將構造方法宣告成為private,那麼暫時無法顯式的呼叫到構造方法了,但是真的沒有其他方法可以破壞單例了麼?

答案是有!也就是透過反射呼叫構造方法,修改許可權。

比如一個看似完美的單例模式:

import java.io.Serializable;public class Singleton{    private volatile static Singleton singleton;    private Singleton(){}    public static Singleton getSingleton() {        if (singleton == null) {            synchronized (Singleton.class) {                if (singleton == null) {                    singleton = new Singleton();                }            }        }        return singleton;    }}

測試程式碼如下:

import java.lang.reflect.Constructor;public class SingletonTests {    public static void main(String[] args) throws Exception {        Singleton singleton = Singleton.getSingleton();        Singleton singleton1=Singleton.getSingleton();        Constructor constructor=Singleton.class.getDeclaredConstructor(null);        constructor.setAccessible(true);        Singleton singleton2 =(Singleton) constructor.newInstance(null);        System.out.println(singleton.hashCode());        System.out.println(singleton1.hashCode());        System.out.println(singleton2.hashCode());    }}

執行結果:

6924040366924040361554874502

從結果我們可以看出:放射確實可以呼叫到已經私有化的構造器,並且構造出不同的物件,從而破壞單例模式。

那這種情況有沒有什麼方法可以防止破壞呢?既然要防止破壞,肯定要防止呼叫私有構造器,也就是呼叫一次之後,再呼叫就報錯,丟擲異常。我們的單例模式可以寫成這樣:

import java.io.Serializable;public class Singleton {    private static int num = 0;    private volatile static Singleton singleton;    private Singleton() {        synchronized (Singleton.class) {            if (num == 0) {                num++;            } else {                throw new RuntimeException("Don't use this method");            }        }    }    public static Singleton getSingleton() {        if (singleton == null) {            synchronized (Singleton.class) {                if (singleton == null) {                    singleton = new Singleton();                }            }        }        return singleton;    }}

測試呼叫方法不變,測試結果如下,反射呼叫的時候丟擲異常了,說明能夠有效阻止反射呼叫破壞單例的模式:

Exception in thread "main" java.lang.reflect.InvocationTargetException    at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)    at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)    at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)    at java.lang.reflect.Constructor.newInstance(Constructor.java:423)    at singleton.SingletonTests.main(SingletonTests.java:11)Caused by: java.lang.RuntimeException: Don't use this method    at singleton.Singleton.<init>(Singleton.java:15)    ... 5 more
2.3 實現了cloneable介面

如果單例物件已經將構造方法宣告成為private,並且重寫了構造方法,那麼暫時無法呼叫到構造方法。但是還有一種情況,那就是複製,複製的時候是不需要經過構造方法的。但是要想複製,必須實現Clonable方法,而且需要重寫clone方法。

import java.io.Serializable;public class Singleton implements Cloneable {    private static int num = 0;    private volatile static Singleton singleton;    private Singleton() {        synchronized (Singleton.class) {            if (num == 0) {                num++;            } else {                throw new RuntimeException("Don't use this method");            }        }    }    public static Singleton getSingleton() {        if (singleton == null) {            synchronized (Singleton.class) {                if (singleton == null) {                    singleton = new Singleton();                }            }        }        return singleton;    }    @Override    protected Object clone() throws CloneNotSupportedException {        return super.clone();    }}

測試程式碼如下:

public class SingletonTests {    public static void main(String[] args) throws Exception {        Singleton singleton1=Singleton.getSingleton();        System.out.println(singleton1.hashCode());        Singleton singleton2 = (Singleton) singleton1.clone();        System.out.println(singleton2.hashCode());    }}

執行結果如下,兩個物件的hashCode不一致,也就證明了如果繼承了Cloneable介面的話,並且重寫了clone()方法,則該類的單例就可以被打破,可以創建出不同的物件。但是,這個clone的方式破壞單例,看起來更像是自己主動破壞單例模式,什麼意思?

也就是如果很多時候,我們只想要單例,但是有極少的情況,我們想要多個物件,那麼我們就可以使用這種方式,更像是給自己留了一個後門,可以認為是一種良性的破壞單例的方式。

2.4 序列化破壞單例

序列化,實際上和clone差不多,但是不一樣的地方在於我們很多物件都是必須實現序列化介面的,但是實現了序列化介面之後,對單例的保證有什麼風險呢?

風險就是序列化之後,再反序列化回來,物件的內容是一樣的,但是物件卻不是同一個物件了。不信?那就試試看:

單例定義如下:

import java.io.Serializable;public class Singleton implements Serializable {    private static int num = 0;    private volatile static Singleton singleton;    private Singleton() {        synchronized (Singleton.class) {            if (num == 0) {                num++;            } else {                throw new RuntimeException("Don't use this method");            }        }    }    public static Singleton getSingleton() {        if (singleton == null) {            synchronized (Singleton.class) {                if (singleton == null) {                    singleton = new Singleton();                }            }        }        return singleton;    }}

測試程式碼如下:

import java.io.*;import java.lang.reflect.Constructor;public class SingletonTests {    public static void main(String[] args) throws Exception {        Singleton singleton1 = Singleton.getSingleton();        ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("file"));        objectOutputStream.writeObject(singleton1);        File file = new File("tempFile");        ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(file));        Singleton singleton2 = (Singleton) objectInputStream.readObject();        System.out.println(singleton1.hashCode());        System.out.println(singleton2.hashCode());    }}

上面的程式碼,先將物件序列化到檔案,再從檔案反序列化回來,結果如下:

20552810211198108795

結果證明:兩個物件的hashCode不一樣,說明這個類的單例被破壞了。

那麼有沒有方法在這種情況下,防止單例的破壞呢?答案是:有!!!

既然呼叫的是objectInputStream.readObject()來反序列化,那麼我們看看裡面的原始碼,裡面呼叫了readObject()方法。

    public final Object readObject()        throws IOException, ClassNotFoundException {        return readObject(Object.class);    }

readObject()方法,裡面呼叫了readObject0()方法:

    private final Object readObject(Class<?> type)        throws IOException, ClassNotFoundException    {        if (enableOverride) {            return readObjectOverride();        }        if (! (type == Object.class || type == String.class))            throw new AssertionError("internal error");        // if nested read, passHandle contains handle of enclosing object        int outerHandle = passHandle;        try {              // 序列化物件            Object obj = readObject0(type, false);            handles.markDependency(outerHandle, passHandle);            ClassNotFoundException ex = handles.lookupException(passHandle);            if (ex != null) {                throw ex;            }            if (depth == 0) {                vlist.doCallbacks();            }            return obj;        } finally {            passHandle = outerHandle;            if (closed && depth == 0) {                clear();            }        }    }

readObject0()內部如下,其實是針對不同的型別分別處理:

    private Object readObject0(Class<?> type, boolean unshared) throws IOException {        boolean oldMode = bin.getBlockDataMode();        if (oldMode) {            int remain = bin.currentBlockRemaining();            if (remain > 0) {                throw new OptionalDataException(remain);            } else if (defaultDataEnd) {                /*                 * Fix for 4360508: stream is currently at the end of a field                 * value block written via default serialization; since there                 * is no terminating TC_ENDBLOCKDATA tag, simulate                 * end-of-custom-data behavior explicitly.                 */                throw new OptionalDataException(true);            }            bin.setBlockDataMode(false);        }        byte tc;        while ((tc = bin.peekByte()) == TC_RESET) {            bin.readByte();            handleReset();        }        depth++;        totalObjectRefs++;        try {            switch (tc) {                // null                case TC_NULL:                    return readNull();                // 引用型別                case TC_REFERENCE:                    // check the type of the existing object                    return type.cast(readHandle(unshared));                // 類                case TC_CLASS:                    if (type == String.class) {                        throw new ClassCastException("Cannot cast a class to java.lang.String");                    }                    return readClass(unshared);                // 代理                case TC_CLASSDESC:                case TC_PROXYCLASSDESC:                    if (type == String.class) {                        throw new ClassCastException("Cannot cast a class to java.lang.String");                    }                    return readClassDesc(unshared);                case TC_STRING:                case TC_LONGSTRING:                    return checkResolve(readString(unshared));                // 陣列                case TC_ARRAY:                    if (type == String.class) {                        throw new ClassCastException("Cannot cast an array to java.lang.String");                    }                    return checkResolve(readArray(unshared));                // 列舉                case TC_ENUM:                    if (type == String.class) {                        throw new ClassCastException("Cannot cast an enum to java.lang.String");                    }                    return checkResolve(readEnum(unshared));                // 物件                case TC_OBJECT:                    if (type == String.class) {                        throw new ClassCastException("Cannot cast an object to java.lang.String");                    }                    return checkResolve(readOrdinaryObject(unshared));                // 異常                case TC_EXCEPTION:                    if (type == String.class) {                        throw new ClassCastException("Cannot cast an exception to java.lang.String");                    }                    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");                    }                case TC_ENDBLOCKDATA:                    if (oldMode) {                        throw new OptionalDataException(true);                    } else {                        throw new StreamCorruptedException(                            "unexpected end of block data");                    }                default:                    throw new StreamCorruptedException(                        String.format("invalid type code: %02X", tc));            }        } finally {            depth--;            bin.setBlockDataMode(oldMode);        }    }

可以看到處理物件的時候,呼叫了readOrdinaryObject()方法,好傢伙來了:

    private Object readOrdinaryObject(boolean unshared)        throws IOException    {        if (bin.readByte() != TC_OBJECT) {            throw new InternalError();        }        ObjectStreamClass desc = readClassDesc(false);        desc.checkDeserialize();        Class<?> cl = desc.forClass();        if (cl == String.class || cl == Class.class                || cl == ObjectStreamClass.class) {            throw new InvalidClassException("invalid class descriptor");        }        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);        }        passHandle = handles.assign(unshared ? unsharedMarker : obj);        ClassNotFoundException resolveEx = desc.getResolveException();        if (resolveEx != null) {            handles.markException(passHandle, resolveEx);        }        if (desc.isExternalizable()) {            readExternalData((Externalizable) obj, desc);        } else {            readSerialData(obj, desc);        }        handles.finish(passHandle);          // 如果實現了hasReadResolveMethod()方法        if (obj != null &&            handles.lookupException(passHandle) == null &&            desc.hasReadResolveMethod())        {              // 執行hasReadResolveMethod()方法            Object rep = desc.invokeReadResolve(obj);            if (unshared && rep.getClass().isArray()) {                rep = cloneArray(rep);            }            if (rep != obj) {                // Filter the replacement object                if (rep != null) {                    if (rep.getClass().isArray()) {                        filterCheck(rep.getClass(), Array.getLength(rep));                    } else {                        filterCheck(rep.getClass(), -1);                    }                }                handles.setObject(passHandle, obj = rep);            }        }        return obj;    }

從上面的diamante可以看出,底層是透過反射來實現序列化的,那我們如果不希望它進行反射怎麼辦?然後可以看到反射之後,其實有一個查詢readResolveMethod()方法有關,如果有實現readResolveMethod(),那就直接呼叫該方法返回結果,而不是返回反射呼叫之後的結果。這樣雖然反射了,但是不起作用。

那要是我們重寫readResolveMethod()方法,就可以直接返回我們的物件,而不是返回反射之後的物件了。

試試?

我們將單例模式改造成為這樣:

import java.io.Serializable;public class Singleton implements Serializable,Cloneable {    private static int num = 0;    private volatile static Singleton singleton;    private Singleton() {        synchronized (Singleton.class) {            if (num == 0) {                num++;            } else {                throw new RuntimeException("Don't use this method");            }        }    }    public static Singleton getSingleton() {        if (singleton == null) {            synchronized (Singleton.class) {                if (singleton == null) {                    singleton = new Singleton();                }            }        }        return singleton;    }    @Override    protected Object clone() throws CloneNotSupportedException {        return super.clone();    }      // 阻止反序列反射生成物件    private Object readResolve() {        return singleton;    }}

測試程式碼不變,結果如下,事實證明確實是這樣,反序列化不會重新反射物件了,一直是同一個物件,問題完美解決了。

20552810212055281021
3. 小結

一個稍微完美的單例,是不會讓別人呼叫構造器的,但是private的構造器,並不能完全阻止對單例的破壞,如果使用反射還是可以非法呼叫到構造器,因為我們需要一個次數,構造器如果呼叫次數過多,那麼就直接報錯。

但是有時候我們希望留個小後門,所以我們大部分時候不可以破壞單例模式。透過實現cloneable的方式,重寫了clone()方法,就可以做到,生成不同的物件。

序列化和clone(),有點像,都是主動提供破壞的方法,但是很多時候不得已提供序列化介面,卻不想被破壞,這個時候可以透過重寫readResolve()方法,直接返回物件,不返回反射生成的物件,保護了單例模式不被破壞。

22
最新評論
  • BSA-TRITC(10mg/ml) TRITC-BSA 牛血清白蛋白改性標記羅丹明
  • 一文秒殺三道括號相關的題目