本系列,記錄了我深入學習設計模式的過程。也算是JAVA進階學習的一個重要知識點吧。java
與設計相關的代碼會貼出,可是基礎功能的代碼會快速帶過。有任何錯誤的地方,都歡迎讀者評論指正,感謝。沖沖衝!設計模式
只須要一個實例存在的場景安全
好比各類Manager 類學習
好比各類Factory 類優化
總得來講,一共有8種單例的實現方式。其中只有2種是完美解決單例問題的。this
其餘6種都有他的缺點。下面會經過先講述6個實現並解決他們的缺點的方式,一個個來說實現。spa
先來看看它的實現.net
1 public class Mrg01 { 2 // static類型保證在classLoader裝載這個類時,就實例化了這個對象 3 private static final Mgr01 INSTANCE = new Mgr01(); 4 /** 5 //這是第二種寫法 6 //這樣的寫法和上面單句的寫法是一個意思,只是初始化寫在靜態語句塊裏 7 private static final Mgr01 INSTANCE = null; 8 static { 9 this.INSTANCE = new Mgr01(); 10 } 11 **/ 12 13 // 私有構造方法避免其餘類new出該對象 14 private Mgr01() {} 15 // 想要拿到這個類的實例,就得用該類靜態公共方法獲取 16 public static Mgr01 getInstance() {return INSTANCE;} 17 // 這個類的任意方法 18 public void m() {System.out.println("do method");} 19 }
優勢:線程
很簡單清晰的實現,在平常的開發中也推薦這種方式來實現單例模式,由於這種方式簡單實用。設計
這種實現是線程安全的,由於JVM只會初始化一次INSTANCE對象。
缺點:
惟一的缺點就是無論這個類是否被用到,它都會被完成實例化。
懶漢式相比上面的餓漢式,它主要是彌補餓漢式的缺點,因而就有下面代碼
1 public class Mgr03 { 2 private static Mgr03 INSTANCE; 3 private Mgr03() {} 4 // 當調用該靜態方法時,根據對象是否爲空,決定是否new一個對象出來,最後返回該對象 5 public static Mgr03 getInstance() { 6 if (INSTANCE == null) { 7 INSTANCE = new Mgr03(); 8 } 9 return INSTANCE; 10 } 11 12 public void m() {System.out.println("do method");} 13 }
看似這樣的實現解決了餓漢式的提早初始化問題,可是卻帶來了線程不安全問題。
緣由很簡單,當首個線程Thread1進入靜態方法時,此時INSTANCE==null,程序會執行new Mgr03(),可是此時線程Thread2也進入靜態方法,但Thread1還未完成INSTANCE初始化,那麼Thread2也會去new Mgr03()。這樣就在首次使用時沒法保證該類的單例存在。
爲了解決懶漢式的線程不安全問題,咱們嘗試用synchronized解決它
1 public class Mgr04 { 2 private static Mgr04 INSTANCE; 3 private Mgr04() {} 4 // 此方法加上synchronized修飾之後,該代碼塊變成同步代碼塊(加鎖) 5 // 如此一來就能夠保證線程順序執行該方法,也就解決了線程不安全問題 6 public static synchronized Mgr04 getInstance() { 7 if (INSTANCE == null) { 8 INSTANCE = new Mgr04(); 9 } 10 return INSTANCE; 11 } 12 public void m() {System.out.println("do method");} 13 }
看似又解決了問題,可是因爲同步鎖的存在,使得這個靜態方法的效率急劇降低。由於每一個線程必須在獲得鎖之後才能獲取到對象,得不到鎖就只能阻塞線程,能不慢嗎(只是相比下很慢)。
1 public class Mgr05 { 2 private static Mgr05 INSTANCE; 3 private Mgr05() {} 4 // 這一次,我把同步代碼塊放在if (INSTANCE==null) 後面 5 // 代碼行數標出來方便下面理解 6 public static Mgr05 getInstance() { //4 7 if (INSTANCE == null) { //5 8 synchronized (Mgr05.class) { //6 9 INSTANCE = new Mgr05(); //7 10 } //8 11 } //9 12 return INSTANCE; //10 13 } //11 14 15 public void m() {System.out.println("do method");} 16 }
這樣的寫法,由於先檢查是否已經實例化INSTANCE,再加鎖在初始化代碼塊上,又又看似解決了上面的效率問題,其實又帶來了新的問題。
首先首次Thread1,Thread2 幾乎同時進入到getInstance方法中,兩個線程執行到第5行,INSTANCE爲空,Thread1先拿到第6行代碼的鎖,Thread2等待鎖釋放,Thread1執行第7行,而後釋放鎖,以後Thread2得到鎖,也執行第7行。這樣INSTANCE就被初始化了2次,仍然線程不安全。
若是檢查一次不行,那就再檢查一次如何
1 public class Mgr06 { 2 private static volatile Mgr06 INSTANCE; 3 // private static Mgr06 INSTANCE; 4 // volatile 關鍵字能夠保證多個線程初始化時,CPU指令能順序執行 5 private Mgr06() {} 6 public static Mgr06 getInstance() { 7 // 雙重檢查 8 if (INSTANCE == null) { 9 synchronized (Mgr06.class) { 10 if (INSTANCE == null) { 11 INSTANCE = new Mgr06(); 12 } 13 } 14 } 15 return INSTANCE; 16 } 17 18 public void m() {System.out.println("do method");} 19 }
雙重檢查下,這樣的單例模式就沒有上面的各類問題了。
可是!關於DCL單例需不須要加volatile關鍵字的問題,這裏還須要額外說明一下。
因爲JVM是無法保證CPU指令順序執行的(容許亂序執行),不加上volatile關鍵字就會可能出現意外。
而volatile 關鍵字能夠保證多個線程初始化時,CPU指令能順序執行
須要知道的是instance = new Mgr05();這句代碼並非一個原子操做,他的操做大致上能夠被拆分爲三步
1.分配內存空間
2.實例化對象instance
3.把instance引用指向已分配的內存空間,此時instance有了內存地址,再也不爲null了
java是容許對指令進行重排序, 那麼以上的三步的執行順序就有多是1-3-2. 在這種狀況下, 若是線程A執行完1-3以後被阻塞了, 而剛好此時線程B進來了 此時的instance已經不爲空了因此線程B判斷完INSTANCE == null 結果爲 false 之後就直接返回了這個尚未實例化好的instance, 因此在調用其後續的實例方法時就會得不到預期的結果
具體能夠參考該文章:https://blog.csdn.net/ACreazyCoder/aicle/details/80982578
雖然這算是個完美的解決方法。可是代碼上仍是不夠簡潔。
先來看代碼
public class Mgr07() { private Mgr07() {} // 加載外部類時不會加載內部類,這樣能夠實現懶加載 // 同時JVM也保證了靜態對象的單例性質 private static class Mgr07Holder { private final static Mgr07 INSTANCE = new Mgr07(); } public static getInstance() { return Mgr07Holder.INSTANCE; } public void m() {System.out.println("do method");} }
定義了一個靜態內部類,當JVM加載Mgr07這個類時,是不會加載其內部類的,也就是說,只有調用getInstance方法時,纔會加載Mgr07Holder,同時初始化INSTANCE對象。
先看代碼
public enum Mgr08 { INSTANCE; public void m{System.out.println("do method");} }
超級精簡的實現。這是SUN發佈的《Effective JAVA》中推薦的一種寫法。利用枚舉類型的特性。是完美的解決方式之一。
這樣寫還能夠解決反序列化從新生成新對象的問題。