前面我們已經講解了設計模式的七大設計原則,今天我們就來聊一聊設計模式中的單例設計模式,看看如何從小小單例模式衍生出來一個大世界。
單例設計模式最簡單的兩個形態分為“懶漢式”和“餓漢式”,顧名思義,懶漢式就是基於事件驅動去載入,俗稱懶載入,餓漢式就是提前載入。
餓漢式
public class Singletion { private Singletion() { } private static Singletion singletion = new Singletion(); public static Singletion getInstance() { return singletion; } }
上面我們是通過靜態變數的形式例項化一次,我們可不可以換一種下面的形式呢,我們把建立單例物件放在靜態程式碼塊中,可以實現相同的效果
public class Singletion { private Singletion() { } static{ singletion = new Singletion(); } private static Singletion singletion; public static Singletion getInstance() { return singletion; } }
看完餓漢式我們再看下懶漢式
public class Singletion { private Singletion() { } private static Singletion singletion; public static Singletion getInstance() { if(singletion==null){ return new Singletion(); } return singletion; } }
但是這種單例有一個很明顯的問題,執行緒不安全,在多執行緒環境下,很有可能建立多個物件,我們需要改進一下,加鎖,沒錯就是加鎖
public class Singletion { private Singletion() { } private static Singletion singletion; public static synchronized Singletion getInstance() { if(singletion==null){ return new Singletion(); } return singletion; } }
那麼我把鎖加在方法上怎麼樣呢,如果被一個外國人看到,我想他一定會說:噢,天哪,這真是個糟糕的決定.為什麼說是個糟糕的決定呢?
在我們實際開發中,一個方法中一定有許多程式碼要執行,我們僅僅只是想同步建立單例物件這一部分程式碼,而我們卻把鎖加在了方法上.
這就好比一個廁所有多個坑位,我們只是想在每個坑位的門上加鎖,而你卻把鎖加在了廁所門上,當進去一個人就把廁所鎖上,這樣顯然不太合適,因此我就再次改造了一下
public class Singletion { private Singletion() { } private static Singletion singletion; public static Singletion getInstance() { if (singletion == null) { synchronized (Singletion.class) { if (singletion == null) { return new Singletion(); } } } return singletion; } }
這樣我們使用過加鎖和雙重檢查機制解決了多執行緒不安全的問題,事情真的就萬事大吉了?如果讓一個外國人看到,我想他會說:噢,天哪,這真是個糟糕的決定.
這裡我們就要聊一聊JVM.編譯器和處理器的指令重排序和物件建立了
物件建立在我們new的時候到底做了些什麼呢?
1:為物件分配記憶體空間
2:初始化物件
3:將物件指向分配的記憶體空間
而指令重排序做了一系列優化,物件建立的過程順序很有可能1->3->2,那麼在多執行緒環境下很有可能執行緒1執行了 1,3還沒有執行2初始化物件,另一個執行緒呼叫getInstance方法發現單例物件不為null,直接返回單例物件,但是此時單例物件還沒有執行2,也就是物件初始化.
為了解決指令重排序可能產生的影響,我們需要在本單例物件的靜態變數上加上volitaile關鍵字修飾才能保證後續不會出問題.
public class Singletion { private Singletion() { } private static volatile Singletion singletion; public static Singletion getInstance() { if (singletion == null) { synchronized (Singletion.class) { if (singletion == null) { return new Singletion(); } } } return singletion; } }
那麼這樣就結束了?有沒有辦法讓JVM幫助我們保證執行緒安全呢?畢竟我不想寫太多程式碼,接下來我們聊一聊靜態內部類
public class Singletion { private Singletion() { } private static class SingletonInside{ private static final Singletion SGT=new Singletion(); } public static Singletion getInstance() { return SingletonInside.SGT; } }
才能保證後續不會出問題.這種方式採用類載入的機制來保證執行緒安全,並且實現了懶載入,只有訪問靜態內部類才回去載入靜態內部類.
這樣就萬無一失了?這時如果一個外國人看到,我想他會說:噢,天啊,這真是個糟糕的決定.
如果此刻我要用反射去建立物件,還能說萬無一失嗎,在反射的基礎上,上面的單例全部都可以推翻?
Constructor<Singletion> constructor = Singletion.class.getDeclaredConstructor(); constructor.setAccessible(true); Singletion instance1 = constructor.newInstance(); Singletion instance2 = constructor.newInstance(); System.out.println(instance1); System.out.println(instance2);
你會驚訝的發現,兩次物件建立的地址不一樣,建立了兩個不一樣地址的物件
那麼如何才能防止反射建立呢?
public class Singletion { private Singletion() { if (SingletonInside.SGT != null) { throw new RuntimeException("不允許反射建立!"); } } private static class SingletonInside{ private static final Singletion SGT=new Singletion(); } public static Singletion getInstance() { return SingletonInside.SGT; } }
這樣就可以了嗎?如果實現了序列化介面,我們通過序列化和反序列化還能拿到同一個物件嗎?
Singletion instance = Singletion.getInstance();
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("src/main/resources/temp.txt")); oos.writeObject(instance1); oos.close(); //反序列化 ObjectInputStream ois = new ObjectInputStream( new FileInputStream("src/main/resources/temp.txt")); Singletion instance2 = (Singletion)ois.readObject();
我們通過比較地址會發現,序列化後的物件會建立另一個記憶體地址,我們如何防止序列化呢? readResolve方法序列化,我們就直接返回物件
public class Singletion implements Serializable { private Singletion() { if (SingletonInside.SGT != null) { throw new RuntimeException("不允許反射建立!"); } } private static class SingletonInside{ private static final Singletion SGT=new Singletion(); } public static Singletion getInstance() { return SingletonInside.SGT; } private Object readResolve(){ return SingletonInside.SGT; } }
目前單例模式還有最後一種終級方案,可以解決上述問題
public enum Singleton implements Serializable{ SGT; public void say(){ System.out.println("我是列舉單例"); } }
這種方式是 Effective Java 作者 Josh Bloch (喬什布洛赫)提倡的方式,但是無法實現延時載入,那麼我們究竟該如何在實際開發中選用呢,根據業務場景選用合適的單例模式,
沒有最好的單例,只有最合適的單例.下一章我們將會聊一聊簡單工廠模式和抽象工廠設計模式