「每天一個知識點」深刻理解單例模式

點擊上方"java全棧技術"關注,每天學習一個java知識點

「每天一個知識點」深刻理解單例模式

「每天一個知識點」深刻理解單例模式

————— 第二天 —————

「每天一個知識點」深刻理解單例模式

「每天一個知識點」深刻理解單例模式

「每天一個知識點」深刻理解單例模式

「每天一個知識點」深刻理解單例模式

「每天一個知識點」深刻理解單例模式

「每天一個知識點」深刻理解單例模式

單例模式第一版:

「每天一個知識點」深刻理解單例模式

為什麼這樣寫呢?我們來解釋幾個關鍵點:

1.要想讓一個類只能構建一個對象,自然不能讓它隨便去做new操作,因此Signleton的構造方法是私有的。

2.instance是Singleton類的靜態成員,也是我們的單例對象。它的初始值可以寫成Null,也可以寫成new Singleton()。至於其中的區別後來會做解釋。

3.getInstance是獲取單例對象的方法。

如果單例初始值是null,還未構建,則構建單例對象並返回。這個寫法屬於單例模式當中的懶漢模式。

如果單例對象一開始就被new Singleton()主動構建,則不再需要判空操作,這種寫法屬於餓漢模式

這兩個名字很形象:餓漢主動找食物吃,懶漢躺在地上等著人喂。

「每天一個知識點」深刻理解單例模式

「每天一個知識點」深刻理解單例模式

為什麼說剛才的代碼不是線程安全呢?

假設Singleton類剛剛被初始化,instance對象還是空,這時候兩個線程同時訪問getInstance方法:

「每天一個知識點」深刻理解單例模式

因為Instance是空,所以兩個線程同時通過了條件判斷,開始執行new操作:

「每天一個知識點」深刻理解單例模式

這樣一來,顯然instance被構建了兩次。讓我們對代碼做一下修改:

單例模式第二版:

「每天一個知識點」深刻理解單例模式

為什麼這樣寫呢?我們來解釋幾個關鍵點:

1.為了防止new Singleton被執行多次,因此在new操作之前加上Synchronized 同步鎖,鎖住整個類(注意,這裡不能使用對象鎖)。

2.進入Synchronized 臨界區以後,還要再做一次判空。因為當兩個線程同時訪問的時候,線程A構建完對象,線程B也已經通過了最初的判空驗證,不做第二次判空的話,線程B還是會再次構建instance對象。

「每天一個知識點」深刻理解單例模式

「每天一個知識點」深刻理解單例模式

「每天一個知識點」深刻理解單例模式

「每天一個知識點」深刻理解單例模式

「每天一個知識點」深刻理解單例模式

像這樣兩次判空的機制叫做雙重檢測機制

「每天一個知識點」深刻理解單例模式

「每天一個知識點」深刻理解單例模式

「每天一個知識點」深刻理解單例模式

「每天一個知識點」深刻理解單例模式

「每天一個知識點」深刻理解單例模式

————————————

「每天一個知識點」深刻理解單例模式

「每天一個知識點」深刻理解單例模式

「每天一個知識點」深刻理解單例模式

「每天一個知識點」深刻理解單例模式

假設這樣的場景,當兩個線程一先一後訪問getInstance方法的時候,當A線程正在構建對象,B線程剛剛進入方法:

「每天一個知識點」深刻理解單例模式

這種情況表面看似沒什麼問題,要麼Instance還沒被線程A構建,線程B執行 if(instance == null)的時候得到true;要麼Instance已經被線程A構建完成,線程B執行 if(instance == null)的時候得到false。

真的如此嗎?答案是否定的。這裡涉及到了JVM編譯器的指令重排

指令重排是什麼意思呢?比如java中簡單的一句 instance = new Singleton,會被編譯器編譯成如下JVM指令:

memory =allocate(); //1:分配對象的內存空間

ctorInstance(memory); //2:初始化對象

instance =memory; //3:設置instance指向剛分配的內存地址

但是這些指令順序並非一成不變,有可能會經過JVM和CPU的優化,指令重排成下面的順序:

memory =allocate(); //1:分配對象的內存空間

instance =memory; //3:設置instance指向剛分配的內存地址

ctorInstance(memory); //2:初始化對象

當線程A執行完1,3,時,instance對象還未完成初始化,但已經不再指向null。此時如果線程B搶佔到CPU資源,執行 if(instance == null)的結果會是false,從而返回一個

沒有初始化完成的instance對象。如下圖所示:

「每天一個知識點」深刻理解單例模式

「每天一個知識點」深刻理解單例模式

如何避免這一情況呢?我們需要在instance對象前面增加一個修飾符volatile。

單例模式第三版:

「每天一個知識點」深刻理解單例模式

「每天一個知識點」深刻理解單例模式

「每天一個知識點」深刻理解單例模式

The volatile keyword indicates that a value may change between different accesses, it prevents an optimizing compiler from optimizing away subsequent reads or writes and thus incorrectly reusing a stale value or omitting writes.

「每天一個知識點」深刻理解單例模式

「每天一個知識點」深刻理解單例模式

經過volatile的修飾,當線程A執行instance = new Singleton的時候,JVM執行順序是什麼樣?始終保證是下面的順序:

memory =allocate(); //1:分配對象的內存空間

ctorInstance(memory); //2:初始化對象

instance =memory; //3:設置instance指向剛分配的內存地址

如此在線程B看來,instance對象的引用要麼指向null,要麼指向一個初始化完畢的Instance,而不會出現某個中間態,保證了安全。

「每天一個知識點」深刻理解單例模式

「每天一個知識點」深刻理解單例模式

「每天一個知識點」深刻理解單例模式

用靜態內部類實現單例模式:

「每天一個知識點」深刻理解單例模式

這裡有幾個需要注意的點:

1.從外部無法訪問靜態內部類LazyHolder,只有當調用Singleton.getInstance方法的時候,才能得到單例對象INSTANCE。

2.INSTANCE對象初始化的時機並不是在單例類Singleton被加載的時候,而是在調用getInstance方法,使得靜態內部類LazyHolder被加載的時候。因此這種實現方式是利用classloader的加載機制來實現懶加載,並保證構建單例的線程安全。

「每天一個知識點」深刻理解單例模式

「每天一個知識點」深刻理解單例模式

「每天一個知識點」深刻理解單例模式

「每天一個知識點」深刻理解單例模式

如何利用反射打破單例模式的約束?其實很簡單,我們來看下代碼。

利用反射打破單例:

//獲得構造器Constructor con = Singleton.class.getDeclaredConstructor();//設置為可訪問con.setAccessible(true);//構造兩個不同的對象Singleton singleton1 = (Singleton)con.newInstance();Singleton singleton2 = (Singleton)con.newInstance();//驗證是否是不同對象System.out.println(singleton1.equals(singleton2));

代碼可以簡單歸納為三個步驟:

第一步,獲得單例類的構造器。

第二步,把構造器設置為可訪問。

第三步,使用newInstance方法構造對象。

最後為了確認這兩個對象是否真的是不同的對象,我們使用equals方法進行比較。毫無疑問,比較結果是false。

「每天一個知識點」深刻理解單例模式

「每天一個知識點」深刻理解單例模式

用枚舉實現單例模式:

public enum SingletonEnum { INSTANCE;}
「每天一個知識點」深刻理解單例模式

「每天一個知識點」深刻理解單例模式

讓我們來做一個實驗,仍然執行剛才的反射代碼:

//獲得構造器Constructor con = SingletonEnum.class.getDeclaredConstructor();//設置為可訪問con.setAccessible(true);//構造兩個不同的對象SingletonEnum singleton1 = (SingletonEnum)con.newInstance();SingletonEnum singleton2 = (SingletonEnum)con.newInstance();//驗證是否是不同對象System.out.println(singleton1.equals(singleton2));

執行獲得構造器這一步的時候,拋出瞭如下異常:

Exception in thread "main" java.lang.NoSuchMethodException: com.xiaohui.singleton.test.SingletonEnum.()

at java.lang.Class.getConstructor0(Class.java:2892)

at java.lang.Class.getDeclaredConstructor(Class.java:2058)

at com.xiaohui.singleton.test.SingletonTest.main(SingletonTest.java:22)

at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)

at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)

at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)

at java.lang.reflect.Method.invoke(Method.java:606)

at com.intellij.rt.execution.application.AppMain.main(AppMain.java:134)

「每天一個知識點」深刻理解單例模式

「每天一個知識點」深刻理解單例模式

「每天一個知識點」深刻理解單例模式

「每天一個知識點」深刻理解單例模式

「每天一個知識點」深刻理解單例模式

幾點補充:

1. volatile關鍵字不但可以防止指令重排,也可以保證線程訪問的變量值是主內存中的最新值。有關volatile的詳細原理,我在以後的漫畫中會專門講解。

2.使用枚舉實現的單例模式,不但可以防止利用反射強行構建單例對象,而且可以在枚舉類對象被反序列化的時候,保證反序列的返回結果是同一對象。

對於其他方式實現的單例模式,如果既想要做到可序列化,又想要反序列化為同一對象,則必須實現readResolve方法。


分享到:


相關文章: