第三夢 單例模式
初識單例
單例模式,算是我們代碼中經常遇見的設計模式之一了。當然我們也上手很快,但是其中的坑也不少,不好好研究一下,這些坑還真不好跳過去。單例簡單分分別為懶漢模式、餓漢模式,那我們就從懶漢模式開始吧。
懶漢模式(線程非安全)
這裏定義一個私有的全局變量singletonPattern,然後通過一個公有的靜態方法對singletonPattern進行判空,如果為空,就new一個類對象出來,然後返回該對象。該種方式可以實現類對象在使用的時候才創建,也就是延時加載。
1 public class SingletonPattern { 2 3 private static SingletonPattern singletonPattern = null; 4 5 private SingletonPattern() { 6 } 7 8 public static SingletonPattern getInstance(){ 9 // if這裏存在競態條件 10 if(singletonPattern == null){ 11 singletonPattern = new SingletonPattern(); 12 } 13 return singletonPattern; 14 } 15 }
懶漢模式(線程安全、低效)
一種比較簡單的方式,是同步獲取實例化的方法getInstance(),也就是加上synchronized關鍵字。當然這種方式是非常低效的(jdk後面的版本對synchronized關鍵字段的底層代碼做了很強的優化,所以也不是不可以考慮),具體如下:
1 public class SingletonPattern { 2 3 private static SingletonPattern singletonPattern = null; 4 5 private SingletonPattern() { 6 } 7 8 public staticsynchronized SingletonPattern getInstance(){ 9 if(singletonPattern == null){ 10 singletonPattern = new SingletonPattern(); 11 } 12 return singletonPattern; 13 } 14 }
懶漢模式(線程安全、高效)
1、雙重鎖校驗DCL(半成品,問題代碼,面試考點),註意看下面羅列的四步。
1 public class SingletonPatternDcl { 2 3 private static SingletonPatternDcl singletonPatternDcl = null; 4 5 private SingletonPatternDcl() { 6 } 7 8 public static SingletonPatternDcl getInstance(){ 9 if(singletonPatternDcl == null){ //1、在實例化的情況下,不需要執行加鎖動作,性能提高 10 synchronized (SingletonPatternDcl.class){ //2、對類上鎖,多個線程的情況下,只有一個線程能夠創建對象 11 if(singletonPatternDcl == null){ //3、實例化對象為空的情況下創建對象 12 singletonPatternDcl = new SingletonPatternDcl(); //4、創建對象 13 } 14 } 15 } 16 return singletonPatternDcl; 17 } 18 }
2、完美的DCL。上面的DCl看起來是非常完美的,所有的邏輯都考慮到了,但是上面的第四步singletonPatternDcl = new SingletonPatternDcl()創建對象的過程其實並非是一個原子操作,這就導致了問題的產生。我們來分析一下第四步在JVM中具體做了哪些事情:
- a、給singletonPatternDcl分配內存空間
- b、調用SingletonPatternDcl的構造函數來初始化該成員變量
- c、將singletonPatternDcl對象指向a步驟分配的內存空間(這一步執行完之後,singletonPatternDcl就為非null了)
而在JVM的即時編譯器中存在指令重排序的優化,如果c步驟在b步驟之前執行的話:b執行了,singletonPatternDcl不為空了,第二個線程來了,發現singletonPatternDcl已經不為null了,然後直接返回。但是其實這個時候singletonPatternDcl只是一個內存地址,根本還沒有初始化,程序就理所當然的報錯了。解決的方法很簡單,基於volatile解決方案,如下所示:
private static volatile SingletonPatternDcl singletonPatternDcl = null;
volatile的特性禁止指令重排序,保證了上述a、b、c一定會按著abc的順序執行,也就避免了上述產生問題的場景。
餓漢模式(天然的線程安全)
利用類加載的機制,我們可以在類一開始加載的時候就初始化一個實例對象。缺點是無法實現懶加載,並且在某些需要使用動態參數的情況下無法使用。
1 public class SingletonPatternSafe { 2 3 private static SingletonPatternSafe singletonPatternSafe = new SingletonPatternSafe(); 4 5 private SingletonPatternSafe() { 6 } 7 8 public SingletonPatternSafe getInstance() { 9 return singletonPatternSafe; 10 } 11 }
這裏加上final也是可以的
private static final SingletonPatternSafe singletonPatternSafe = new SingletonPatternSafe();
靜態內部類(天然的線程安全)
這種方式的單例實現,也是基於JVM本身機制保證了線程安全。其內部類Holder只有getInstance()方法可以訪問。讀取的實例的時候也不需要進行同步,沒有性能的損失。
1 public class SingletonPatternHolder { 2 3 private static class Holder { 4 private static final SingletonPatternHolder INSTANCE = new SingletonPatternHolder(); 5 } 6 7 private SingletonPatternHolder() { 8 } 9 10 public static SingletonPatternHolder getInstance(){ 11 return Holder.INSTANCE; //懶漢式的,只有訪問getInstance()方法的時候才實例化 12 } 13 14 }
枚舉方式(絕對的線程安全)
枚舉實現單例模式有三個特性:自由序列化、線程安全、保證單例。
- enum的實現是通過繼承了Enum類來實現的,enum結構不能作為子類來繼承其他類,但是可以用來實現接口類;
- 由於enum內部的實現方式其實是final類型的,所以enum類不可以被繼承;
- enum有且僅有private構造器,防止外部的額外構造,這恰好和單例模式相符合;
- 其內部也是枚舉量未被初始化,之後會在靜態代碼中進行初始化,這就非常類似餓漢模式;
- 對於序列化和反序列化,因為每一個枚舉類型和枚舉變量在JVM中都是唯一的,所以Java在序列化和反序列化枚舉時做了特殊規定,枚舉的writeObject、readObject、readReplace和readResolve等方式是被編譯器禁止的,因此不存在實現序列化接口之後調用readObject會重新創建的心得對象從而破壞單例的問題。
基於上述描述,我們發現enum的方式來構造單例模式,代碼實現起來非常的簡單、自由序列化。並且也是線程的安全,相比起來應該更優選擇
1 public enum SingletonPatternEnum { 2 3 /** 4 * 實例化對象 5 */ 6 INATANCE 7 }
代碼實例
我的代碼放在GitHub,小夥伴可以作為一個參考、
參考博文
- Java枚舉enum以及應用:枚舉實現單例模式
- 如何正確地寫出單例模式
- 死磕Java並發:Java內存模型之從JMM角度分析DCL
SingletonPattern、我們平常用到的一個設計模式,有必要深入學習,掌握精髓,在實戰中靈活運用。感謝前輩們的分享做為引路人。-------書山有路、人兒需行<<
第三夢 單例模式