您好,登錄后才能下訂單哦!
在之前的?設計模式 - 單例模式(詳解)看看和你理解的是否一樣??一文中,我們提到了通過Idea
?開發工具進行多線程調試、單例模式的破壞的問題;由于篇幅原因,現在單獨開一篇文章進行演示:線程不安全的單例在多線程情況下為何被創建多個、如何破壞單例。
如果還不知道如何使用IDEA工具進行線程模式的調試,請先閱讀我之前發的一篇文章:?你不知道的 IDEA Debug調試小技巧
首先回顧簡單線程不安全的懶漢式單例的代碼以及測試程序代碼:
/** ?*?@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; ????} }//?測試程序@Testpublic?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
模式來直觀的找出答案。
單例類LazySimpleSingleton
的if (instance == null)
?處:
測試類,多線程入口調用getInstance()
處:鄭州不孕不育醫院性價比哪家好:http://yyk.39.net/zz3/zonghe/1d427.html
啟動?debug
?,我們可以在調試窗口找到我們啟動的線程:
將?pool-1-thread-1
?線程單步執行到if (instance == null)
?斷點處,觀察instance
值為null
;
將pool-1-thread-1
執行到instance = new LazySimpleSingleton();
處等待初始化:http://www.chacha8.cn/detail/1132398246.html
切換線程?pool-1-thread-2
?同樣單步執行到?if (instance == null)
?斷點處,此時觀察instance
值也為null
(這就是我們常說的兩個線程同時執行到斷代碼處):
同樣將pool-1-thread-2
執行到instance = new LazySimpleSingleton();
處等待初始化:
顯然,這兩個線程都滿足if (instance == null)
?的條件,都應該到對應的代碼塊中執行實例化操作,那么這兩個線程就會分別初始化:
線程?pool-1-thread-1
?實例化后:
切換線程?pool-1-thread-2
?觀察?instance
?值已經被初始化了,但是,線程pool-1-thread-2
?還是會被實例化一遍:
線程pool-1-thread-2
實例化后:
大家是否一目了然了呢?
將兩個線程執行完,看控制臺:
大家可以看到,雖然輸出打印的對象是同一個,但是,確實是創建了兩遍,只不過?pool-1-thread-2
?實例化后將?pool-1-thread-1
實例化的對象值給覆蓋了。
當我將線程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
為例,編寫反射調用測試代碼:
@Testpublic?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 對象類找到一個唯一的枚舉對象。因此,枚舉對象不可能被類加載器加載多次。
那么是否可以通過反射進行破壞呢?我們先來執行以下反射破壞枚舉類的測試代碼:
@Testpublic?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; }
那我們來做一個這樣的測試:
@Testpublic?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
?枚舉的語法特殊性,以及反射也為枚舉保駕護航,讓枚舉式單例成為一種比較優雅的實現。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。