在以前的 設計模式 - 單例模式(詳解)看看和你理解的是否同樣? 一文中,咱們提到了經過Idea
開發工具進行多線程調試、單例模式的暴力破壞的問題;因爲篇幅緣由,如今單獨開一篇文章進行演示:線程不安全的單例在多線程狀況下爲什麼被建立多個、如何破壞單例。java
若是還不知道如何使用IDEA工具進行線程模式的調試,請先閱讀我以前發的一篇文章: 你不知道的 IDEA Debug調試小技巧git
首先回顧簡單線程不安全的懶漢式單例的代碼以及測試程序代碼:程序員
/** * @author eamon.zhang * @date 2019-09-30 上午10:55 */ public class LazySimpleSingleton { private LazySimpleSingleton(){} private static LazySimpleSingleton instance = null; public static LazySimpleSingleton getInstance(){ if (instance == null) { instance = new LazySimpleSingleton(); } return instance; } } // 測試程序 @Test public void test() { try { ConcurrentExecutor.execute(() -> { LazySimpleSingleton instance = LazySimpleSingleton.getInstance(); System.out.println(Thread.currentThread().getName() + " : " + instance); }, 2, 2); } catch (Exception e) { e.printStackTrace(); } }
對於這個單例,咱們毫無疑問認爲它是線程不安全的,至於爲何,接下來使用IDEA
工具的線程debug
模式來直觀的找出答案。github
LazySimpleSingleton
的if (instance == null)
處:getInstance()
處:debug
,咱們能夠在調試窗口找到咱們啓動的線程:pool-1-thread-1
線程單步執行到if (instance == null)
斷點處,觀察instance
值爲null
;pool-1-thread-1
執行到instance = new LazySimpleSingleton();
處等待初始化:pool-1-thread-2
一樣單步執行到 if (instance == null)
斷點處,此時觀察instance
值也爲null
(這就是咱們常說的兩個線程同時執行到斷代碼處):pool-1-thread-2
執行到instance = new LazySimpleSingleton();
處等待初始化:if (instance == null)
的條件,都應該到對應的代碼塊中執行實例化操做,那麼這兩個線程就會分別初始化:線程 pool-1-thread-1
實例化後:segmentfault
切換線程 pool-1-thread-2
觀察 instance
值已經被初始化了,可是,線程pool-1-thread-2
仍是會被實例化一遍:設計模式
線程pool-1-thread-2
實例化後:安全
你們是否一目瞭然了呢?多線程
你們能夠看到,雖然輸出打印的對象是同一個,可是,確實是建立了兩遍,只不過 pool-1-thread-2
實例化後將 pool-1-thread-1
實例化的對象值給覆蓋了。ide
當我將線程pool-1-thread-1
和線程pool-1-thread-2
同時執行到instance = new LazySimpleSingleton();
處而後先讓pool-1-thread-1
執行完打印後,再將pool-1-thread-2
執行實例化操做,就會看到打印的對象會是不同的了:工具
這就是經過線程調試模式手動控制線程執行順序來模擬還原多線程環境下,線程不安全的狀況。
咱們明白了線程不安全的緣由是兩個線程同時拿到的instance
資源都爲null
,從而都進行實例化。那麼有沒有什麼方法能解決呢?固然有,給 getInstance()
加 上 synchronized
關鍵字,使這個方法變成線程同步方法:
public class LazySimpleSingleton { private LazySimpleSingleton(){} private static LazySimpleSingleton instance = null; public synchronized static LazySimpleSingleton getInstance(){ if (instance == null) { instance = new LazySimpleSingleton(); } return instance; } }
當咱們將其中一個線程執行並調用 getInstance()
方法時,另外一個線程在調用 getInstance()
方法,線程的狀態由 RUNNING
變成了MONITOR
,出現阻塞。直到第一個線程執行完,第二個線程才恢復 RUNNING
狀態繼續調用 getInstance()
方法
這就解決了以前所說的線程安全問題,可是這樣子在線程數量比較多狀況下,若是 CPU
分配壓力上升,會致使大批量線程出現阻塞,從而致使程序運行性能大幅降低;爲了解決線程安全和程序性能問題,因而乎有了咱們的雙重檢查式的單例。這裏就再也不多說了。
通常狀況下,咱們建立使用餓漢式單例或雙重檢查的懶漢式單例是沒有問題的,可是在必定狀況下,會發生單例被破壞。
實際狀況下,公司一個程序員寫了一個單例,可是另一個程序員,可能比較牛 X,寫代碼風格有點不同,他經過反射來調用別人寫的接口,這就會出現此單例並不是彼單例的狀況。這就破壞了單例。
在咱們寫單例的時候,你們有沒有注意到私有的構造方法前面的修飾符僅爲 private
,若是咱們使用反射來調用其構造方法,而後,再調用 getInstance()
方法,應該就會有兩個不一樣的實例。
咱們之前面說單例的文章中的 LazyInnerClassSingleton
爲例,編寫反射調用測試代碼:
@Test public void testReflex() { try { // 很無聊的狀況下,進行破壞 Class<LazyInnerClassSingleton> clazz = LazyInnerClassSingleton.class; // 經過反射拿到私有的構造方法 Constructor<LazyInnerClassSingleton> c = clazz.getDeclaredConstructor(null); // 設置訪問屬性,強制訪問 c.setAccessible(true); // 暴力初始化兩次,這就至關於調用了兩次構造方法 LazyInnerClassSingleton o1 = c.newInstance(); LazyInnerClassSingleton o2 = c.newInstance(); // 只要 o1和o2 地址不相等,就能夠說明這是兩個不一樣的對象,也就是違背了單例模式的初衷 System.out.println(o1 == o2); } catch (Exception e) { e.printStackTrace(); } }
運行結果以下:
顯然,是建立了兩個不一樣的實例。如今,咱們在其構造方法中作一些限制,一旦出現屢次重複建立,則直接拋出異常。來看優化後的代碼:
public class LazyInnerClassSingleton { private LazyInnerClassSingleton() { if(LazyHolder.INSTANCE != null){ throw new RuntimeException("不容許建立多個實例"); } } // 注意關鍵字final,保證方法不被重寫和重載 public static final LazyInnerClassSingleton getInstance() { return LazyHolder.INSTANCE; } private static class LazyHolder { // 注意 final 關鍵字(保證不被修改) private static final LazyInnerClassSingleton INSTANCE = new LazyInnerClassSingleton(); } }
再次調用:
至此,就避免了單例被反射破壞的問題。
另一種狀況,可能會遇到,咱們須要將對象序列化到磁盤,下次使用時再從磁盤反序列化回來,反序列化的對象會被從新分配內存,那若是序列化的對象爲單例,則就違背了單例模式的初衷。這也至關於破壞了單例。
咱們仍是以LazyInnerClassSingleton
爲例,將LazyInnerClassSingleton
實現 Serializable
接口;
而後編寫測試代碼:
/** * @author eamon.zhang * @date 2019-10-08 下午3:06 */ public class SerializableTest { public static void main(String[] args) { LazyInnerClassSingleton s1 = null; LazyInnerClassSingleton s2 = LazyInnerClassSingleton.getInstance(); FileOutputStream fos = null; try { fos = new FileOutputStream("LazyInnerClassSingleton.obj"); ObjectOutputStream oos = new ObjectOutputStream(fos); oos.writeObject(s2); oos.flush(); oos.close(); FileInputStream fis = new FileInputStream("LazyInnerClassSingleton.obj"); ObjectInputStream ois = new ObjectInputStream(fis); s1 = (LazyInnerClassSingleton)ois.readObject(); ois.close(); System.out.println(s1); System.out.println(s2); } catch (Exception e) { e.printStackTrace(); } } }
執行測試代碼:
能夠看到,結果爲兩個不一樣的對象。這一樣違背了單例模式的初衷。那麼咱們如何保證序列化的狀況也能實現單例呢?其實也很簡單,使用 readResolve()
方法便可:
public class LazyInnerClassSingleton implements Serializable { private LazyInnerClassSingleton() { if (LazyHolder.INSTANCE != null) { throw new RuntimeException("不容許建立多個實例"); } } // 注意關鍵字final,保證方法不被重寫和重載 public static final LazyInnerClassSingleton getInstance() { return LazyHolder.INSTANCE; } private static class LazyHolder { // 注意 final 關鍵字(保證不被修改) private static final LazyInnerClassSingleton INSTANCE = new LazyInnerClassSingleton(); } // 解決反序列化對象不一致問題 private Object readResolve() { return LazyHolder.INSTANCE; } }
你們確定會問,why?
爲了一探究竟,咱們來看一下 JDK 源碼,咱們進入 ObjectInputStream
類的 readObject()
方法:
public final Object readObject() throws IOException, ClassNotFoundException { if (this.enableOverride) { return this.readObjectOverride(); } else { int outerHandle = this.passHandle; Object var4; try { Object obj = this.readObject0(false); this.handles.markDependency(outerHandle, this.passHandle); ClassNotFoundException ex = this.handles.lookupException(this.passHandle); if (ex != null) { throw ex; } if (this.depth == 0L) { this.vlist.doCallbacks(); this.freeze(); } var4 = obj; } finally { this.passHandle = outerHandle; if (this.closed && this.depth == 0L) { this.clear(); } } return var4; } }
咱們發現:readObject 中又調用了咱們重寫的 readObject0()
方法,進入 readObject0()
方法:
private Object readObject0(boolean unshared) throws IOException { ... try { switch(tc) { ... case 115: var4 = this.checkResolve(this.readOrdinaryObject(unshared)); return var4; ... } finally { --this.depth; this.bin.setBlockDataMode(oldMode); } return var4; }
咱們看到代碼中調用了 ObjectInputStream
的 readOrdinaryObject()
方法,咱們繼續進入看源碼:
private Object readOrdinaryObject(boolean unshared) throws IOException { ... if (cl != String.class && cl != Class.class && cl != ObjectStreamClass.class) { Object obj; try { obj = desc.isInstantiable() ? desc.newInstance() : null; } catch (Exception var7) { throw (IOException)(new InvalidClassException(desc.forClass().getName(), "unable to create instance")).initCause(var7); } ... } }
發現調用了 ObjectStreamClass
的 isInstantiable()
方法,而 isInstantiable()
裏面的代碼以下:
boolean isInstantiable() { this.requireInitialized(); return this.cons != null; }
代碼很是簡單,就是判斷一下構造方法是否爲空,構造方法不爲空就返回 true
,也就是說,只要有無參構造方法就會實例化;這時候,其實尚未找到爲何加上readResolve()
方法就避免了單例被破壞的真正緣由,咱們再次回到ObjectInputStream
的 readOrdinaryObject()
方法繼續往下看能夠找到以下代碼:
private Object readOrdinaryObject(boolean unshared) throws IOException { ... if (obj != null && this.handles.lookupException(this.passHandle) == null && desc.hasReadResolveMethod()) { Object rep = desc.invokeReadResolve(obj); if (unshared && rep.getClass().isArray()) { rep = cloneArray(rep); } if (rep != obj) { if (rep != null) { if (rep.getClass().isArray()) { this.filterCheck(rep.getClass(), Array.getLength(rep)); } else { this.filterCheck(rep.getClass(), -1); } } obj = rep; this.handles.setObject(this.passHandle, rep); } } ... }
判斷無參構造方法是否存在以後,又調用了 hasReadResolveMethod()
方法:
boolean hasReadResolveMethod() { this.requireInitialized(); return this.readResolveMethod != null; }
邏輯很是簡單,就是判斷readResolveMethod
是否爲空,不爲空就返回 true
。那麼 readResolveMethod
是在哪裏賦值的呢? 經過全局查找找到了賦值代碼在私有方法 ObjectStreamClass()
方法中給 readResolveMethod
進行賦值,來看代碼:
ObjectStreamClass.this.readResolveMethod = ObjectStreamClass.getInheritableMethod(cl, "readResolve", (Class[])null, Object.class);
代碼的邏輯其實就是經過反射找到一個無參的 readResolve()
方法,而且保存下來,如今再回到 ObjectInputStream
的 readOrdinaryObject()
方法繼續往下看,若是readResolve()
存在則調用 invokeReadResolve()
方法:
Object invokeReadResolve(Object obj) throws IOException, UnsupportedOperationException { this.requireInitialized(); if (this.readResolveMethod != null) { try { return this.readResolveMethod.invoke(obj, (Object[])null); } catch (InvocationTargetException var4) { Throwable th = var4.getTargetException(); if (th instanceof ObjectStreamException) { throw (ObjectStreamException)th; } else { throwMiscException(th); throw new InternalError(th); } } catch (IllegalAccessException var5) { throw new InternalError(var5); } } else { throw new UnsupportedOperationException(); } }
咱們能夠看到在 invokeReadResolve()
方法中用反射調用了 readResolveMethod()
方法。 經過JDK
源碼分析咱們能夠看出,雖然,增長 readResolve()
方法返回實例,解決了單例被破壞的問題。可是,咱們經過分析源碼以及調試,咱們能夠看到實際上實例化了兩 次,只不過新建立的對象沒有被返回而已.
那若是,建立對象的動做發生頻率增大,就 意味着內存分配開銷也就隨之增大;爲了解決這個問題,咱們推薦使用註冊式單例。
咱們在前文中說到了,咱們極力推薦使用枚舉類型的單例;接下來咱們分析一下緣由:
使用 Java
反編譯工具 Jad
(自行下載),解壓後,使用命令行調用:
./jad ~/IdeaProjects/own/java-advanced/01.DesignPatterns/design-patterns/build/classes/java/main/com/eamon/javadesignpatterns/singleton/enums/EnumSingleton.class
會在當前目錄生成一個 EnumSingleton.jad
文件,咱們使用 vscode
打開這個文件查看:
public final class EnumSingleton extends Enum { public static EnumSingleton[] values() { return (EnumSingleton[])$VALUES.clone(); } public static EnumSingleton valueOf(String name) { return (EnumSingleton)Enum.valueOf(com/eamon/javadesignpatterns/singleton/enums/EnumSingleton, name); } private EnumSingleton(String s, int i) { super(s, i); instance = new EnumResource(); } public Object getInstance() { return instance; } public static final EnumSingleton INSTANCE; private Object instance; private static final EnumSingleton $VALUES[]; static { INSTANCE = new EnumSingleton("INSTANCE", 0); $VALUES = (new EnumSingleton[] { INSTANCE }); } }
請注意這段代碼:
static { INSTANCE = new EnumSingleton("INSTANCE", 0); $VALUES = (new EnumSingleton[] { INSTANCE }); }
原來枚舉類單例在靜態代碼塊中就給INSTANCE
賦了值,是餓漢式單例的實現方式。那麼一樣的,咱們可否經過反射和序列化方式進行破壞呢?
先分析經過序列化方式:
咱們仍是回到JDK
源碼:在 ObjectInputStream
的 readObject0()
方法中有以下代碼:
private Object readObject0(boolean unshared) throws IOException { ... case 126: var4 = this.checkResolve(this.readEnum(unshared)); ... return var4; }
咱們看到 readObject0()
中調用了readEnum()
方法,跟進該方法:
private Enum<?> readEnum(boolean unshared) throws IOException { if (this.bin.readByte() != 126) { throw new InternalError(); } else { ObjectStreamClass desc = this.readClassDesc(false); if (!desc.isEnum()) { throw new InvalidClassException("non-enum class: " + desc); } else { int enumHandle = this.handles.assign(unshared ? unsharedMarker : null); ClassNotFoundException resolveEx = desc.getResolveException(); if (resolveEx != null) { this.handles.markException(enumHandle, resolveEx); } String name = this.readString(false); Enum<?> result = null; Class<?> cl = desc.forClass(); if (cl != null) { try { Enum<?> en = Enum.valueOf(cl, name); result = en; } catch (IllegalArgumentException var9) { throw (IOException)(new InvalidObjectException("enum constant " + name + " does not exist in " + cl)).initCause(var9); } if (!unshared) { this.handles.setObject(enumHandle, result); } } this.handles.finish(enumHandle); this.passHandle = enumHandle; return result; } } }
咱們發現枚舉類型其實經過類名和 Class 對象類找到一個惟一的枚舉對象。所以,枚舉對象不可能被類加載器加載屢次。
那麼是否能夠經過反射進行破壞呢?咱們先來執行如下反射破壞枚舉類的測試代碼:
@Test public void testEnum(){ try { // 很無聊的狀況下,進行破壞 Class<EnumSingleton> clazz = EnumSingleton.class; // 經過反射拿到私有的構造方法 Constructor<EnumSingleton> c = clazz.getDeclaredConstructor(null); // 設置訪問屬性,強制訪問 c.setAccessible(true); // 暴力初始化兩次,這就至關於調用了兩次構造方法 EnumSingleton o1 = c.newInstance(); EnumSingleton o2 = c.newInstance(); // 只要 o1和o2 地址不相等,就能夠說明這是兩個不一樣的對象,也就是違背了單例模式的初衷 System.out.println(o1 == o2); } catch (Exception e) { e.printStackTrace(); } }
執行結果:
報的是 java.lang.NoSuchMethodException
異常,意思是沒找到無參的構造方法。
那麼咱們來看一下 java.lang.Enum
的源碼,咱們發現它只有一個protected
的構造方法:
protected Enum(String name, int ordinal) { this.name = name; this.ordinal = ordinal; }
那咱們來作一個這樣的測試:
@Test public void testEnum1() { try { Class clazz = EnumSingleton.class; Constructor c = clazz.getDeclaredConstructor(String.class, int.class); c.setAccessible(true); EnumSingleton enumSingleton = (EnumSingleton) c.newInstance("Eamon", 666); } catch (Exception e) { e.printStackTrace(); } }
發現控制檯輸出以下錯誤:
意思就是不能用反射來建立枚舉類型。至於爲何,咱們仍是來看 JDK
源碼,進入Constructor
的newInstance()
方法中:
public T newInstance(Object... initargs) throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException { if (!this.override) { Class<?> caller = Reflection.getCallerClass(); this.checkAccess(caller, this.clazz, this.clazz, this.modifiers); } if ((this.clazz.getModifiers() & 16384) != 0) { throw new IllegalArgumentException("Cannot reflectively create enum objects"); } else { ConstructorAccessor ca = this.constructorAccessor; if (ca == null) { ca = this.acquireConstructorAccessor(); } T inst = ca.newInstance(initargs); return inst; } }
原來,在源碼中對枚舉類型進行了強制性的判斷(16384
表明枚舉類型),若是是枚舉類型,直接拋異常。到此爲止也就說明了爲何《Effective Java》推薦使用枚舉來實現單例的緣由: JDK
枚舉的語法特殊性,以及反射也爲枚舉保駕護航,讓枚舉式單例成爲一種比較優雅的實現。
本文中所涉及的源碼可在 github 上找到,相關的測試代碼在 test 包下:https://github.com/eamonzzz/java-advanced