1. 程式人生 > >Java的23種設計模式----單例模式

Java的23種設計模式----單例模式

Java的23種設計模式——單例模式

1、引子

單例模式是設計模式中使用很頻繁的一種模式,在各種開源框架、應用系統中多有應用,並對有人提出的“單例模式是邪惡的”這個觀點進行了一定的分析。

2、定義與結構


  • 單例模式又叫做“單態模式”或者“單件模式”
  • 在GOF書中給出的定義為:
    1. 保證一個類僅有一個例項,並提供一個訪問它的全域性訪問點
  • 單例模式中的“單例”通常用來代表那些本質上具有唯一性的系統元件(或者叫做資源)。比如檔案系統、資源管理器等等。
  • 單例模式的目的就是要控制特定的類只產生一個物件,當然也允許在一定情況下靈活的改變物件的個數
  • 那麼如何來實現單例模式呢?(一個類的物件的產生,是由類建構函式來完成的,如果想限制物件的產生,)
    1. 將建構函式變為私有的(至少是受保護的),使的外面的類不能通過引用來產生物件
    2. 同時為了保證類的可用性,就必須提供一個自己的物件以及訪問這個物件的靜態方法

現在對單例模式有了大概的瞭解了吧,其實單例模式在實現上是非常簡單的——只有一個角色,而客戶則通過呼叫類方法來得到類的物件
這裡寫圖片描述
單例模式可分為有狀態的和無狀態的。有狀態的單例物件一般也是可變的單例物件,多個單態物件在一起就可以作為一個狀態倉庫一樣向外提供服務。沒有狀態的單例物件也就是不變單例物件,僅用做提供工具函式

3、程式碼實現

  • 在單例模式中的實現上有幾種不同的方式。

3.1、餓漢式

  • 《Java與模式》中被稱為“餓漢式”
/**
 * 餓漢式
 */
public
class SingleDemo { //在自己內部定義自己一個例項 //注意這裡是private,只供內部使用 private static SingleDemo instance=new SingleDemo(); //如上面所述,將建構函式私有化 private SingleDemo(){ } //靜態工廠方法,提供了一個供外部訪問得到物件的方法 public static SingleDemo getInstance(){ return instance; } }

3.2、懶漢式

/**
 * 懶漢式
 */
public class SingleDemo { //在自己內部定義自己一個例項 //注意這裡是private,只供內部使用 //和上面有什麼不同? private static SingleDemo instance=null; //將建構函式私有化 private SingleDemo(){ } //靜態工廠方法 public static synchronized SingleDemo getInstance(){ //這個方法比“餓漢式”有所改進 if(instance==null){ instance= new SingleDemo(); } return instance; } }

3.3、兩者對比

  • “餓漢式”和“懶漢式”有什麼不同呢?
  • 首先對比一下實現方式

    1. 建構函式都是私有化,無法使用建構函式來得到類的例項,但是這樣也使得類失去了多型性(大概這就是為什麼有人喜歡將這種模式稱之為‘單態模式’的原因吧!)
    2. 懶漢式中,對靜態方法工廠進行了同步處理,為了防止多執行緒環境中產生多個例項,餓漢式中不存在這種情況
    3. 餓漢式中類載入的時候例項化,這樣對此載入會造成對此例項化,懶漢式使用了同步處理,在反應速度上會比餓漢式慢一些
  • 在 《java 與模式》書中提到,就 java 語言來說,第一種方式更符合 java 語言本身的特點。

以上兩種實現方式均失去了多型性,不允許被繼承。

3.4、附加(登錄檔方式)

還有另外一種靈活點的實現,將建構函式設定為受保護的,這樣允許被繼承產生子類。這種方式在具體實現上又有所不同,可以將父類中獲得物件的靜態方法放到子類中再實現;也可以在父類的靜態方法中進行條件判斷來決定獲得哪一個物件;在 GOF 中認為最好的一種方式是維護一張存有物件和對應名稱的登錄檔(可以使用 HashMap 來實現)。

  • 下面的實現參考《java 與模式》採用帶有登錄檔的方式
import java.util.HashMap;

/**
 * 《java與模式》採用帶有登錄檔的方式
 */
public class SingleDemo {
    //用來存放對應關係
    private static HashMap sinRegistry =new HashMap();

    static private SingleDemo s =new SingleDemo();

    //受保護的建構函式
    protected SingleDemo(){

    }

    public static SingleDemo getInstance(String name){
        if(name==null){
            name="Singleton";
        }

        if(sinRegistry.get(name)==null){
            try {
                sinRegistry.put(name, Class.forName(name).newInstance());
            } catch (Exception e) {
                e.getStackTrace();
            }
        }
        return (SingleDemo)(sinRegistry.get(name));
    }
}
/**
 * SingleDemoChild
 */
public class SingleDemoChild extends SingleDemo{

    public SingleDemoChild(){

    }

    static public SingleDemoChild getInstance(){
        return (SingleDemoChild)SingleDemo.getInstance("SingleDemoChild");
    }
}
  • 由於在 java 中子類的建構函式的範圍不能比父類的小,所以可能存在不守規則的客戶程式使用其建構函式來產生例項,造成單例模式失效。

4、單例模式邪惡論

  • 單例模式在 java 中的使用存在很多陷阱和假象,這使得沒有意識到單例模式使用侷限性的你在系統中佈下了隱患……
  • 下面一一列舉單例模式的陷阱
  • 多個虛擬機器

當系統中的單例類被拷貝執行在多個虛擬機器下的時候,在每一個虛擬機器下都可以建立一個例項物件。在使用了EJB、JINI、RMI技術的分散式系統中,由於中介軟體遮蔽掉了分散式系統在物理上的差異,所以對於你來說,想知道具體哪個虛擬機器下執行著哪個單例是很困難的。

因此,在使用以上分散式技術系統中,應該避免使用存在狀態的單例模式,因為一個有狀態的單例類,在不同的虛擬機器上,各個單例物件儲存的狀態很可能是不一樣的,問題也就隨之產生。而且在 EJB 中不要使用單例模式來控制訪問資源,因為這是由 EJB 容器來負責的。在其它的分散式系統中,當每一個虛擬機器中的資源是不同的時候,可以考慮使用單例模式來進行管理。

  • 多個類載入器

當存在多個類載入器載入類的時候,即使它們載入的是相同包名,相同類名甚至每個位元組都完全相同的類,也會被區別對待的。因為不同的類載入器會使用不同的名稱空間(namespace)來區分同一個類。因此,單例類在多載入器的環境下會產生多個單例物件。也許你認為出現多個類載入器的情況並不是很多。其實多個類載入器存在的情況並不少見。在很多 J2EE 伺服器上允許存在多個 servlet 引擎,而每個引擎是採用不同的類載入器的;瀏覽器中 applet 小程式通過網路載入類的時候,由於安全因素,採用的是特殊的類載入器,等等。這種情況下,由狀態的單例模式也會給系統帶來隱患。因此除非系統由協調機制,在一般情況下不要使用存在狀態的單例模式。

  • 錯誤的同步處理

在使用上面介紹的懶漢式單例模式時,同步處理的恰當與否也是至關重要的。不然可能會達不到得到單個物件的效果,還可能引發死鎖等錯誤。因此在使用懶漢式單例模式時一定要對同步有所瞭解。不過使用餓漢式單例模式就可以避免這個問題。

  • 子類破壞了物件控制

由於類建構函式變得不再私有,就有可能失去對物件的控制。這種情況只能通過良好的文件來規範

  • 序列化(可序列化)

為了使一個單例類變成可序列化的,僅僅在宣告中新增“implements Serializable”是不夠的。因為一個序列化的物件在每次返序列化的時候,都會建立一個新的物件,而不僅僅是一個對原有物件的引用。為了防止這種情況,可以在單例類中加入 readResolve 方法。 關於這個方法的具體情況請參考《Effective Java》一書第 57 條建議。其實物件的序列化並不僅侷限於上述方式,還存在基於 XML 格式的物件序列化方式。這種方式也存在上述的問題,所以在使用的時候要格外小心。

5、題外話

  • 拋開單例模式,使用下面一種簡單的方式也能得到單例,而且如果你確信此類永遠是單例的,使用下面這種方式也許更好一些。
public static final SingleDemo INSTANCE = new SingleDemo();
  • 而使用單例模式提供的方式,這可以在不改變 API 的情況下,改變我們對單例類的具體要求。