設計模式學習整理(二)

單例模式

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

 

單例模式可分爲有狀態的和無狀態的。有狀態的單例對象一般也是可變的單例對象,多個單態對象在一起就可以作爲一個狀態倉庫一樣向外提供服務。沒有狀態的單例對象也就是不變單例對象,僅用做提供工具函數。

實現 
在單例模式的實現上有幾種不同的方式,我在這裏將一一講解。先來看一種方式,它在《java與模式》中被稱爲餓漢式。  
public class Singleton { 
//在自己內部定義自己一個實例 //注意這是private 只供內部調用 
private static Singleton instance = new Singleton(); //如上面所述,將構造函數設置爲私有 
private Singleton(){ }  
//靜態工廠方法,提供了一個供外部訪問得到對象的靜態方法

 

  public static Singleton getInstance() {     return instance;      } 

}  
  下面這種方式被稱爲懶漢式:P  
public class Singleton {   //和上面有什麼不同? 
private static Singleton instance = null; //設置爲私有的構造函數 private Singleton(){ }  
//靜態工廠方法 
public static synchronized Singleton getInstance() { //這個方法比上面有所改進 
   if (instance==null) 
  instance=new Singleton();

return instance;   

}

}   

首先他們的構造函數都是私有的,徹底斷開了使用構造函數來得到類的實例的通道,但是這樣也使得類失去了多態性(大概這就是爲什麼有人將這種模式稱作單態模式)。  
在第二種方式中,對靜態工廠方法進行了同步處理,原因很明顯——爲了防止多線程環境中產生多個實例;而在第一種方式中則不存在這種情況。  在第二種方式中將類對自己的實例化延遲到第一次被引用的時候。而在第一種方式中則是在類被加載的時候實例化,這樣多次加載會照成多次實例化。但是第二種方式由於使用了同步處理,在反應速度上要比第一種慢一些。  在 《java與模式》書中提到,就java語言來說,第一種方式更符合java語言本身的特點。  以上兩種實現方式均失去了多態性,不允許被繼承。還有另外一種靈活點的實現,將構造函數設置爲受保護的,這樣允許被繼承產生子類。這種方式在具體實現上又有所不同,可以將父類中獲得對象的靜態方法放到子類中再實現;也可以在父類的靜態方法中進行條件判斷來決定獲得哪一個對象;在GOF中認爲最好的一種方式是維護一張存有對象和對應名稱的註冊表(可以使用HashMap來實現)。下面的實現參考《java與模式》採用帶有註冊表的方式。

import java.util.HashMap;  
public class Singleton { 
//用來存放對應關係

private static HashMap sinRegistry = new HashMap();  static private Singleton s = new Singleton();  //受保護的構造函數 
protected Singleton()  {}  public static Singleton 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.printStackTrace();     }    } 
  return (Singleton)(sinRegistry.get(name));   }  public void test()  { 
  System.out.println("getclasssuccess!");   } }  
public class SingletonChild1 extends Singleton { 
public SingletonChild1(){}  static  public SingletonChild1 getInstance()  { 
  return (SingletonChild1)Singleton.getInstance("SingletonChild1");   }  public void test()  { 
  System.out.println("getclasssuccess111!");   } }

 

單例模式邪惡論

單例模式在java中的使用存在很多陷阱和假象,這使得沒有意識到單例模式使用侷限性的你在系統中佈下了隱患…… 
其實這個問題早在2001年的時候就有人在網上系統的提出來過,我在這裏只是老生常談了。但是對於大多的初學者來說,可能這樣的觀點在還很陌生。下面我就一一列舉出單例模式在java中存在的陷阱。 
多個虛擬機 
當系統中的單例類被拷貝運行在多個虛擬機下的時候,在每一個虛擬機下都可以創建一個實例對象。在使用了EJB、JINI、RMI技術的分佈式系統中,由於中間件屏蔽掉了分佈式系統在物理上的差異,所以對你來說,想知道具體哪個虛擬機下運行着哪個單例對象是很困難的。 
因此,在使用以上分佈技術的系統中,應該避免使用存在狀態的單例模式,因爲一個有狀態的單例類,在不同虛擬機上,各個單例對象保存的狀態很可能是不一樣的,問題也就隨之產生。而且在EJB中不要使用單例模式來控制訪問資源,因爲這是由EJB容器來負責的。在其它的分佈式系統中,當每一個虛擬機中的資源是不同的時候,可以考慮使用單例模式來進行管理。 
多個類加載器 
當存在多個類加載器加載類的時候,即使它們加載的是相同包名,相同類名甚至每個字節都完全相同的類,也會被區別對待的。因爲不同的類加載器會使用不同的命名空間(namespace)來區分同一個類。因此,單例類在多加載器的環境下會產生多個單例對象。 
也許你認爲出現多個類加載器的情況並不是很多。其實多個類加載器存在的情況並不少見。在很多J2EE服務器上允許存在多個servlet引擎,而每個引擎是採用不同的類加載器的;瀏覽器中applet小程序通過網絡加載類的時候,由於安全因素,採用的是特殊的類加載器,等等。  這種情況下,由狀態的單例模式也會給系統帶來隱患。因此除非系統由協調機制,在一般情況下不要使用存在狀態的單例模式。 
  錯誤的同步處理

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

在上一節介紹最後一種擴展性較好的單例模式實現方式的時候,就提到,由於類構造函數變得不再私有,就有可能失去對對象的控制。這種情況只能通過良好的文檔來規範。

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

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章