前言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()方法,直接返回物件,不返回反射生成的物件,保護了單例模式不被破壞。