1. 程式人生 > >深入淺出設計模式(一):單例模式

深入淺出設計模式(一):單例模式

注:本文參考《深入淺出設計模式》和網上資料,並對某些文字以自己的理解進行了適當的修改。個人覺得本文應作為入門學習,瞭解大體框架,具體的設計模式有待詳細研究。

  • 1. 單一指責原則(SRP,Single Responsibility Principle)
    系統裡的每一個物件都應該只有一個單獨的職責,而所有物件所關注的就是自身職責的完成。每個類應該只有一個職責,對外只能提供一種功能,而引起類變化的原因應該只有一個。

  • 2. 開閉原則(OCP,Open Close Principle)
    對擴充套件開放,對修改關閉。在程式需要進行拓展的時候,不能去修改原有的程式碼,實現一個熱插拔的效果。所以一句話概括就是:為了使程式的擴充套件性好,易於維護和升級。想要達到這樣的效果,我們需要使用介面和抽象類。

  • 3. 依賴注入原則(DIP,Dependence Inversion Principle)
    面對介面程式設計,依賴於抽象而不依賴於具體。

  • 4. 里氏替換原則(LSP,Liskov Substitution Principle)
    任何基類可以出現的地方,子類一定可以出現。 LSP是繼承複用的基石,只有當衍生類可以替換掉基類,軟體單位的功能不受到影響時,基類才能真正被複用,而衍生類也能夠在基類的基礎上增加新的行為。LSP是對“開-閉”原則的補充。實現“開-閉”原則的關鍵步驟就是抽象化。而基類與子類的繼承關係就是抽象化的具體實現,所以里氏代換原則是對實現抽象化的具體步驟的規範。

  • 5. 迪米特法則(最少知道原則)(DP,Demeter Principle)(LOD,Law of Demeter)


    一個物件應當儘量少的與其他物件之間發生相互作用,使得系統功能模組相對獨立(降低各個物件之間的耦合),提高系統可維護性。

  • 6. 介面隔離原則(ISP,Interface Segregation Principle)
    使用多個隔離的介面,比使用單個介面要好。不應該強迫客戶程式依賴它們不需要使用的方法。(一個介面應該只提供一種對外功能,不應該把所有的操作都封裝到一個介面當中)

  • 7.優先使用組合而不是繼承原則(CARP,Composite/Aggregate Reuse Principle)

1. 建立型模式,共五種

  • 單例模式(Singleton)

  • 簡單工廠模式(Simple Factory),此模式並不在23種設計模式之中

  • 工廠方法模式(Factory Method)

  • 抽象工廠模式(Abstract Factory)

  • 原型模式(Prototype)

  • 建立者模式(Builder)

2. 結構型模式,共七種

  • 介面卡模式(Adapter)

  • 門面模式(Facade)

  • 代理模式(Proxy)

  • 合成模式(Composite)

  • 享元模式(Flyweight)

  • 裝飾模式(Decorator)

  • 橋接模式(Bridge)

3. 行為型模式,共11種

  • 策略模式(Strategy)

  • 迭代器模式(Iterator)

  • 模版方法模式(Template Method)

  • 中介者模式(Mediator)

  • 訪問者模式(Vistor)

  • 職責鏈模式(Chain of Responsibility)

  • 狀態模式(State)

  • 直譯器模式(Interpreter)

  • 觀察者模式(Observer)

  • 命令模式(Command)

  • 備忘錄模式(Memento)

這裡寫圖片描述

單例模式在java中的Runtime類、資料庫連線池和日誌管理中典型應用,總結起來就是當程式執行時,需要保證一個物件只有一個例項存在時,就應該用到單例模式。這樣的模式有幾個好處:

  • 某些類建立比較頻繁,對於一些大型的物件,這是一筆很大的系統開銷。

  • 省去了new操作符,降低了系統記憶體的使用頻率,減輕GC壓力。

  • 有些類如交易所的核心交易引擎,控制著交易流程,如果該類可以建立多個的話,系統完全亂了。(比如一個軍隊出現了多個司令員同時指揮,肯定會亂成一團),所以只有使用單例模式,才能保證核心交易伺服器獨立控制整個流程。

餓漢模式:

package singleton;

public class Singleton {    

    private static Singleton instance = new Singleton();  

    /* 私有構造方法,防止被例項化 */  
    private Singleton() {  
    }  

    /* 靜態工程方法,建立例項 */  
    public static Singleton getInstance() {   
        return instance;  
    }   

} 

懶漢模式:

public class Singleton {  

    /* 持有私有靜態例項,防止被引用,此處賦值為null,目的是實現延遲載入 */  
    private static Singleton instance = null;  

    /* 私有構造方法,防止被例項化 */  
    private Singleton() {  
    }  

    /* 靜態工程方法,建立例項 */  
    public static Singleton getInstance() {  
        if (instance == null) {  
            instance = new Singleton();  
        }  
        return instance;  
    }  

    /* 如果該物件被用於序列化,可以保證物件在序列化前後保持一致 */  
    public Object readResolve() {  
        return instance;  
    }  

}  

此懶漢模式在單執行緒的程式應用中是沒有任何問題的,但是在多執行緒程式中就會出現問題(當多個執行緒都進行if(instance == null)判斷時,就會產生多個該類的示例)。我們首先會想到對getInstance方法加synchronized關鍵字,如下:

// 增加同步機制
public static synchronized Singleton getInstance() {  
      if (instance == null) {  
          instance = new Singleton();  
      }  
      return instance;  
} 

但是因為這種寫法是將synchronized機制放在了獲取例項的方法上,導致程式每取一次例項,都將進入synchronized機制,效率低。

事實上,只有在第一次建立物件的時候需要加鎖,之後就不需要了,Double-checked locking(雙檢測鎖)機制:

public static Singleton getInstance() {
    if (instance == null) {
        // 同步機制放在產生例項的程式碼前
        synchronized (instance) {
            if (instance == null) { //對是否為null再次判斷
                instance = new Singleton();
            }
        }
    }
    return instance;
} 

但是,這樣的情況,還是有可能有問題的,看下面的情況:
在Java指令中建立物件和賦值操作是分開進行的,也就是說instance = new Singleton();語句是分兩步執行的。但是JVM並不保證這兩個操作的先後順序,也就是說有可能JVM會為新的Singleton例項分配空間,然後直接賦值給instance成員,然後再去初始化這個Singleton例項。這樣就可能出錯了。

我們以A、B兩個執行緒為例:

  • A、B執行緒同時進入了第一個if判斷
  • A首先進入synchronized塊,由於instance為null,所以它執行instance = new Singleton();
  • 由於JVM內部的優化機制,JVM先畫出了一些分配給Singleton例項的空白記憶體,並賦值給instance成員(注意此時JVM沒有開始初始化這個例項),然後A離開了synchronized塊。
  • B進入synchronized塊,由於instance此時不是null,因此它馬上離開了synchronized塊並將結果返回給呼叫該方法的程式。
  • 此時B執行緒打算使用Singleton例項,卻發現它沒有被初始化,於是錯誤發生了。

所以程式還是有可能發生錯誤,其實程式在執行過程是很複雜的,從這點我們就可以看出,尤其是在寫多執行緒環境下的程式更有難度,有挑戰性。我們對該程式做進一步優化:

public class Singleton {  

    /* 私有構造方法,防止被例項化 */  
    private Singleton() {  
    }  

    /* 此處使用一個內部類來維護單例 */  
    private static class SingletonFactory {  
        private static Singleton instance = new Singleton();  
    }  

    /* 獲取例項 */  
    public static Singleton getInstance() {  
        return SingletonFactory.instance;  
    }  

    /* 如果該物件被用於序列化,可以保證物件在序列化前後保持一致 */  
    public Object readResolve() {  
        return getInstance();  
    }  

}  

其實說它完美,也不一定,如果在建構函式中丟擲異常,例項將永遠得不到建立,也會出錯。所以說,十分完美的東西是沒有的,我們只能根據實際情況,選擇最適合自己應用場景的實現方法。也有人這樣實現:因為我們只需要在建立類的時候進行同步,所以只要將建立和getInstance()分開,單獨為建立加synchronized關鍵字,也是可以的:

public class Singleton {  

    private static Singleton instance = null;  

    private Singleton () {  
    }  

    private static synchronized void syncInit() {  
        if (instance == null) {  
            instance = new Singleton ();  
        }  
    }  

    public static Singleton getInstance() {  
        if (instance == null) {  
            syncInit();  
        }  
        return instance;  
    }  
}  
  • 首先,靜態類不能實現介面。(從類的角度說是可以的,但是那樣就破壞了靜態了。因為介面中不允許有static修飾的方法,所以即使實現了也是非靜態的)

  • 其次,單例可以被延遲初始化,靜態類一般在第一次載入是初始化。之所以延遲載入,是因為有些類比較龐大,所以延遲載入有助於提升效能。

  • 再次,單例類可以被繼承,他的方法可以被覆寫。但是靜態類內部方法都是static,無法被覆寫。

  • 最後一點,單例類比較靈活,畢竟從實現上只是一個普通的Java類,只要滿足單例的基本需求,你可以在裡面隨心所欲的實現一些其它功能,但是靜態類不行。

從上面這些概括中,基本可以看出二者的區別,但是,從另一方面講,我們上面最後實現的那個單例模式,內部就是用一個靜態類來實現的,所以,二者有很大的關聯,只是我們考慮問題的層面不同罷了。兩種思想的結合,才能造就出完美的解決方案,就像HashMap採用陣列+連結串列來實現一樣,其實生活中很多事情都是這樣,單用不同的方法來處理問題,總是有優點也有缺點,最完美的方法是,結合各個方法的優點,才能最好的解決問題!