文章出處

1. 單例模式的定義

        單例模式(Singleton Pattern)是一個比較簡單的模式,其原始定義如下:Ensure a class has only one instance, and provide a global point of access to it. 即確保只有一個實例,而且自行實例化并向整個系統提供這個實例。單例模式的通用類如下圖所示:

        Singleton類稱為單例類,通過使用private的構造函數確保了在一個應用中只產生一個實例,并且是自行實例化的(在Singleton中自己new Singleton())。單例模式的通用代碼如下(這種也稱為餓漢式單例):

/****************** 單例模式:程序清單1 ***************************/  
public class Singleton {  
    private static Singleton instance = new Singleton(); //1.自己內部new一個  
      
    private Singleton() { //2.私有構造函數,防止被實例化  
          
    }  
    //3.提供一個公共接口,用來返回剛剛new出來的對象  
    public static Singleton getInstance() {   
         return instance;  
    }  
      
    public void test() {  
        System.out.println("singleton");  
    }  
}  
/********************************************************************/  

2. 單例模式存在的線程安全問題

        上面是一個經典的單例模式程序,且這個程序不會產生線程同步問題,因為類第一次加載的時候就初始化了instance。但是單例模式還有其他的實現方式,就有可能會出現線程同步問題,請看下面的例子:

/* 
 * 這種方式就是非線程安全了(懶漢式單例) 
 */  
public class Singleton {  
    private static Singleton instance = null;  
    private Singleton() {  
          
    }  
    public static Singleton getInstance() {  
        if(instance == null) {  
            instance = new Singleton();  
        }  
        return instance;  
    }  
} 

  為什么會出現線程安全問題呢?假如一個線程A執行到instance = new Singleton(),但還沒有獲得對象(對象的初始化是需要時間的),第二個線程B也在執行,執行到判斷instance == null時,那么線程B獲得的條件也是真,于是也進入實例化instance了,然后線程A獲得了一個對象,線程B也獲得了一個對象,在內存中就存在了兩個對象了!

        解決線程安全問題的方法有很多,比如我們可以在getInstance()方法前面加上synchronized關鍵字來解決,如下:

public static synchronized Singleton getInstance() {    
        if (instance == null) {    
            instance = new Singleton();    
        }    
        return instance;    
}   

但是synchronized關鍵字鎖住的是這個對象,這樣的用法在性能上會有所下降,因為每次調用getInstance()時都要對對象上鎖。事實上,只要在第一次創建對象的時候加鎖,后面創建完了就不需要了,所以我們可以做進一步的改進,如下:

public static Singleton getInstance() {    
        if (instance == null) {    
            synchronized (instance) {    
                if (instance == null) {    
                    instance = new Singleton();    
                }    
            }    
        }    
        return instance;    
}  

我們將synchronized關鍵字加到內部,也就是說當調用的時候是不需要加鎖的,只有在instance == null的時候且創建對象的時候再加鎖,這樣要比上面的那種方式好。但是這種方式還是有可能會產生線程安全問題,因為JVM中創建對象和賦值操作是分開進行的,即instance = new Singleton()這句是分兩步進行的。過程是這樣的:JVM會為先給Singleton實例分配一個空白的內存,并賦值給instance成員,但是此時JVM并沒有開始初始化這個實例,然后再去new一個Singleton對象賦給instance。這就會導致線程問題了,比如A線程進入synchronized代碼塊了,執行完了instance = new Singleton()后退出代碼塊,但是此時還沒有真正初始化,這是線程B進來了,發現instance不為null,于是就立馬返回該instance(其實是沒有初始化好的),然后B就開始使用該instance,卻發現沒初始化,于是就出問題了。

 

        所以要解決這種“懶漢式”單例的線程問題,一種建議使用上面的程序清單1的方式,即使用”餓漢式“單例。另一種,在實際中,也可以用內部類來維護單例的實現。JVM內部的機制能夠保證當一個類被加載的時候,這個類的加載過程是線程互斥的。這樣,當我們第一次調用getInstance()方法的時候,JVM能夠幫我們保證instance實例只被創建一次,并且會保證把賦值給instance的內存初始化完畢,見如下代碼:

/****************** 單例模式:程序清單2 ****************************/  
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();    
    }     
}  
/********************************************************************/  

3.單例模式的克隆

        上面分析了單例模式的線程安全問題,還有個問題就是需要考慮單例模式中對象的復制問題。在Java中,對象默認是不可以被復制的,但是若實現了Cloneable接口,并實現了clone方法,則可以直接通過對象復制方式創建一個新對象,對象復制不是調用類的構造方法,所以即使是私有的構造方法,對象仍然是可以被復制的。但是在一般情況下,單例類很少會主動要求被復制的,所以解決該問題最好的方法就是單例類不要實現Cloneable接口即可。

4. 單例模式的擴展

        如果一個類可以產生多個對象且數量不受限制,是非常容易的,直接new就是了。但是如果使用單例模式,但是要求一個類真能產生兩三個對象呢?這種情況該如何實現?針對這種情況,我們就需要在單例類中維護一個變量,用來表示實例的個數,而且還需要一些容器來保存不同的實例以及實例對應的屬性,如下:

/*************************** 單例模式的擴展:程序清單3 ************************************/  
public class Singleton {  
    //定義最多能產生的實例數量  
    private static int maxNumOfInstance = 3;  
      
    //存儲每個實例的名字  
    private static ArrayList<String> nameList = new ArrayList<String>();  
      
    //存儲每個實例對象  
    private static ArrayList<Singleton> instanceList = new ArrayList<Singleton>();  
      
    //當前實例的索引  
    private static int indexOfInstance = 0;  
      
    //靜態代碼塊,在類加載的時候初始化2個實例  
    static {  
        for(int i = 0; i < maxNumOfInstance; i++) {  
            instanceList.add(new Singleton("instance" + (i+1)));  
        }  
    }  
      
    private Singleton() {  
          
    }  
    private Singleton(String name) { //帶參數的私有構造函數  
        nameList.add(name);  
    }  
      
    //返回實例對象  
    public static Singleton getInstance() {  
        Random random = new Random();  
        //隨機挑選一個實例  
        indexOfInstance = random.nextInt(maxNumOfInstance);  
        return instanceList.get(indexOfInstance);  
    }  
    public void test() {  
        System.out.println(nameList.get(indexOfInstance));  
    }  
}  
/******************************************************************************************/   

 我們寫一個測試程序看看結果就知道了:

public class SingletonTest {  
  
    public static void main(String[] args) {  
        int num = 5;  
        for(int i = 0; i < num; i++) {  
            Singleton instance = Singleton.getInstance();  
            instance.test();  
        }  
    }  
}  

 這樣我們就實現了用單例模式產生固定數量的實例。測試結果輸出如下:

instance1  
instance1  
instance2  
instance3  
instance3  

5. 單例模式的優缺點

      優點:

        1.在內存中只存在一個實例,所有減小誒村的開支,特別是一個對象需要頻繁的創建和銷毀時,而且創建或銷毀時性能又無法優化,單例模式的優勢就非常明顯;

        2.減小了系統的性能開銷,當一個對象的產生需要比較多的資源時,如讀取配置、產生依賴對象時,則可以通過在應用啟動時直接產生一個單例對象,然后用永久駐留在內存中。

        3.可以避免對資源的多重占用,如寫文件動作,由于只有一個實例存在內存中,避免對同一個資源文件的同時寫操作。

        4.單例模式可以在系統設置全局的訪問點,優化和共享資源訪問,例如可以設計一個單例類,負責所有數據表的映射處理。

      缺點:

        1.單例模式沒有接口,擴展很難,若要擴展,除了修改代碼基本上沒有第二種途徑可以實現

       2.單例模式對測試是不利的,在并行開發環境中,如果單例模式沒有完成,是不能進行測試的。

6. 單例模式的應用場景

      在一個系統中,要求一個類僅有一個對象時,可以采用單例模式:

        1. 要求生成唯一序列號的環境。

        2. 在整個項目中需要一個共享訪問點或共享數據,例如一個web頁面上的訪問量,可以不用每次刷新都把記錄存到數據庫,但是要確保單例線程安全。

        3. 創建一個對象需要消耗的資源過多,如要訪問IO和數據庫等資源。

        4. 需要定義大量的靜態常量和靜態方法(如工具類)的環境,可以采用單例模式,當然也可以直接聲明為static方式。

        spring中也用到了單例模式,每個Bean默認就是單例的,這樣做的有點事Spring容器可以管理這些Bean的生命期,決定什么時候創建出來,什么時候銷毀,銷毀的時候要如何處理等等。如果采用非單例模式(Prototype類型),則Bean初始化后的管理交給J2EE容器了,Spring容器就不在跟蹤管理Bean的生命周期了。


文章列表


不含病毒。www.avast.com
arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()