Java 中的單例設計模式,不少時候咱們只會注意到線程引發的表象性問題,可是沒考慮過對反射機制的限制,此文旨在簡單介紹利用枚舉來防止反射的漏洞。java
咱們先展現一段最多見的懶漢式的單例:設計模式
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;
}
}
複製代碼
上述單例的寫法採用的雙重檢查機制增長了必定的安全性,可是沒有考慮到 JVM 編譯器的指令重排。安全
好比 java 中簡單的一句 instance = new Singleton,會被編譯器編譯成以下 JVM 指令:bash
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,時,準備走2,即 instance 對象還未完成初始化,但已經再也不指向 null 。優化
此時若是線程 B 搶佔到CPU資源,執行 if(instance == null)的結果會是 false,spa
從而返回一個沒有初始化完成的instance對象。線程
如何去防止呢,很簡單,能夠利用關鍵字 volatile 來修飾 instance 對象,以下圖進行優化:設計
why?
很簡單,volatile 修飾符在此處的做用就是阻止變量訪問先後的指令重排,從而保證了指令的執行順序。
意思就是,指令的執行順序是嚴格按照上文的 一、二、3 來執行的,從而對象不會出現中間態。
其實,volatile 關鍵字在多線程的開發中應用很廣,暫不贅述。
雖然很贊,可是此處仍然沒有考慮過反射機制帶來的影響。
實現單例有不少種模式,在此介紹一種使用靜態內部類實現單例模式的方式:
public class Singleton {
private static class LazyHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton (){}
public static Singleton getInstance() {
return LazyHolder.INSTANCE;
}
}
複製代碼
這是一種很巧妙的方式,起因是:
從外部沒法訪問靜態內部類 LazyHolder,只有當調用 Singleton.getInstance() 方法的時候,才能獲得單例對象 INSTANCE。
INSTANCE 對象初始化的時機並非在單例類 Singleton 被加載的時候,而是在調用 getInstance 方法,使得靜態內部類 LazyHolder 被加載的時候。
所以這種實現方式是利用classloader的加載機制來實現懶加載,並保證構建單例的線程安全。
不少種單例的寫法都有一個通病,就是沒法防止反射機制的漏洞,從而沒法保證對象的惟一性,以下舉例:
利用以下的反正代碼對上文構造的單例進行對象的建立。
public static void main(String[] args) {
try {
//得到構造器
Constructor con = Singleton.class.getDeclaredConstructor();
//設置爲可訪問
con.setAccessible(true);
//構造兩個不一樣的對象
Singleton singleton1 = (Singleton)con.newInstance();
Singleton singleton2 = (Singleton)con.newInstance();
//驗證是不是不一樣對象
System.out.println(singleton1);
System.out.println(singleton2);
System.out.println(singleton1.equals(singleton2));
} catch (Exception e) {
e.printStackTrace();
}
}
複製代碼
咱們直接看結果:
結果很明顯,這顯然是兩個對象。
使用枚舉來實現單例模式。
實現很簡單,就三行代碼:
public enum Singleton {
INSTANCE;
}
複製代碼
上面所展現的就是一個單例,
why?
其實這就是 enum 的一塊語法糖,JVM 會阻止反射獲取枚舉類的私有構造方法。
仍然使用上文的反射代碼來進行測試,發現,報錯。嘿嘿,完美解決反射的問題。
使用枚舉的方法是起到了單例的做用,可是也有一個弊端,
那就是 沒法進行懶加載。