1. 程式人生 > >【java設計模式】之 單例(Singleton)模式

【java設計模式】之 單例(Singleton)模式

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的生命週期了。

        單例模式就討論這麼多吧,如有錯誤之處,歡迎留言指正~

文末福利:“程式設計師私房菜”,一個有溫度的公眾號~ 
程式設計師私房菜

_____________________________________________________________________________________________________________________________________________________

-----樂於分享,共同進步!