詳解JAVA面向對象的設計模式 (一)、單例模式

前言

本系列,記錄了我深入學習設計模式的過程。也算是JAVA進階學習的一個重要知識點吧。java

與設計相關的代碼會貼出,可是基礎功能的代碼會快速帶過。有任何錯誤的地方,都歡迎讀者評論指正,感謝。沖沖衝!設計模式

 

單例模式 Singleton

應用場景

只須要一個實例存在的場景安全

  • 好比各類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次,仍然線程不安全。

六、雙重檢查 double-check locking (DCL單例)

若是檢查一次不行,那就再檢查一次如何

 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對象。

這麼一來,既能保證INSTANCE的單例,也能夠實現懶加載。代碼也很簡單清晰。是完美的解決方式之一。

八、枚舉單例

先看代碼

public enum Mgr08 {
    INSTANCE;
    
    public void m{System.out.println("do method");}
}

 

超級精簡的實現。這是SUN發佈的《Effective JAVA》中推薦的一種寫法。利用枚舉類型的特性。是完美的解決方式之一。

這樣寫還能夠解決反序列化從新生成新對象的問題。(並不常見)

相關文章
相關標籤/搜索