什么是单例模式?
單例模式第一版:public class Singleton {private Singleton() {} //私有構造函數private static Singleton instance = null; //單例對象//靜態工廠方法public static Singleton getInstance() {if (instance == null) {instance = new Singleton();}return instance;}
}為什么這樣寫呢?我們來解釋幾個關鍵點:
1.要想讓一個類只能構建一個對象,自然不能讓它隨便去做new操作,因此Signleton的構造方法是私有的。2.instance是Singleton類的靜態成員,也是我們的單例對象。它的初始值可以寫成Null,也可以寫成
new Singleton()。至于其中的區別后來會做解釋。3.getInstance是獲取單例對象的方法。
如果單例初始值是null,還未構建,則構建單例對象并返回。這個寫法屬于單例模式當中的懶漢模式。
如果單例對象一開始就被new Singleton()主動構建,則不再需要判空操作,這種寫法屬于餓漢模式。
這兩個名字很形象:餓漢主動找食物吃,懶漢躺在地上等著人喂。但是上邊單列不是線程安全的單列!為什么說剛才的代碼不是線程安全呢?
假設Singleton類剛剛被初始化,instance對象還是空,這時候兩個線程同時訪問getInstance方法:
因為Instance是空,所以兩個線程同時通過了條件判斷,開始執行new操作:
這樣一來,顯然instance被構建了兩次。讓我們對代碼做一下修改:單例模式第二版:public class Singleton {private Singleton() {} //私有構造函數private static Singleton instance = null; //單例對象//靜態工廠方法public static Singleton getInstance() {if (instance == null) { //雙重檢測機制synchronized (Singleton.class){ //同步鎖if (instance == null) { //雙重檢測機制instance = new Singleton();}}}return instance;}
}為什么這樣寫呢?我們來解釋幾個關鍵點:1.為了防止new Singleton被執行多次,因此在new操作之前加上Synchronized 同步鎖,鎖住整個類
(注意,這里不能使用對象鎖)
2.進入Synchronized 臨界區以后,還要再做一次判空。因為當兩個線程同時訪問的時候,線程A構建完
對象,線程B也已經通過了最初的判空驗證,不做第二次判空的話,線程B還是會再次構建instance對象。像這樣兩次判空的機制叫做雙重檢測機制??傮w上可以,但是這段代碼仍然不是絕對的線程安全!
假設這樣的場景,當兩個線程一先一后訪問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。單例模式第三版:public class Singleton {private Singleton() {} //私有構造函數private volatile static Singleton instance = null; //單例對象//靜態工廠方法public static Singleton getInstance() {if (instance == null) { //雙重檢測機制synchronized (Singleton.class){ //同步鎖if (instance == null) { //雙重檢測機制instance = new Singleton();}}}return instance;}
}用最簡單的方式理解,volatile修飾符阻止了變量訪問前后的指令重排,保證了指令執行順序!經過volatile的修飾,當線程A執行instance = new Singleton的時候,JVM執行順序是什么樣?始終保證
是下面的順序:memory =allocate(); //1:分配對象的內存空間 ctorInstance(memory); //2:初始化對象 instance =memory; //3:設置instance指向剛分配的內存地址 如此在線程B看來,instance對象的引用要么指向null,要么指向一個初始化完畢的Instance,而不會出現
某個中間態,保證了安全。二,實現單列模式的手段非常多,我們先看一看通過靜態內部類實現的單列模式。用靜態內部類實現單例模式:public class Singleton {private static class LazyHolder {private static final Singleton INSTANCE = new Singleton();}private Singleton (){}public static Singleton getInstance() {return LazyHolder.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;
}有了enum,JVM會阻止反射獲取枚舉的私有構造方法。讓我們來做一個實驗,仍然執行剛才的反射代碼://獲得構造器
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:使用枚舉實現的單例模式不僅能夠防止反射構造對象,而且可以保證線程安全。不過這種方式也有唯一缺點,
就是他并非使用懶加載,其單例對象實在枚舉類被加載的時候進行初始化的。基本上單列模式介紹到這里!
下面做一個簡單總結。幾點補充:1. volatile關鍵字不但可以防止指令重排,也可以保證線程訪問的變量值是主內存中的最新值。2.使用枚舉實現的單例模式,不但可以防止利用反射強行構建單例對象,而且可以在枚舉類對象被反序列化
的時候,保證反序列的返回結果是同一對象。
?
總結
- 上一篇: final、finally与finali
- 下一篇: java中ArrayList和Linke