單例模式

1.單例模式

單例模式是應用最廣的模式之一,也可能是很多初級工程師唯一會使用的設計模式。在應用這個模式時,單例對象的類必須保證只有一個實例存在。許多時候整個系統只需要擁有一個全局對象,這樣有利於我們協調系統整體的行爲。

2.單例模式的定義

確保某一個類只有一個實例,而且自行實例化並向整個系統提供這個實例。

3.單例模式的使用場景

確保某個類有且只有一個對象的場景,避免產生多個對象消耗過多資源,或者某種類型的對象只應該有且只有一個。例如,創建一個對象需要消耗的資源過多,如要訪問IO和數據庫等資源,這時就要考慮使用單例模式。

4.單例模式UML類圖

在這裏插入圖片描述
角色介紹:

  • Client:高層客戶端;
  • Singleton:單例類。

實現單例模式主要有如下幾個關鍵點:

  1. 構造函數不對外開放,一般爲Private;
  2. 通過一個靜態類方法或者枚舉返回單例類對象;
  3. 確保單例類的對象有且只有一個,尤其是在多線程環境下;
  4. 確保單例類對象在反序列化時不會重新構造對象。

通過將單例類的構造函數私有化,使得客戶端代碼不能通過new的形式手動構造單例類的對象。單例類會暴露一個公有靜態方法,客戶端需要調用這個靜態方法獲取到單例類的唯一對象,在獲取這個單例對象的過程中需要確保線程安全,即在多線程環境下構造單例類的對象也是有且只有一個,這也是單例模式實現中比較困難的地方。

5.單例模式的簡單實現

單例模式是設計模式中比較簡單的,只有一個單例類,沒有其它的層次結構與抽象。該模式需要確保該類只能生成一個對象,通常是該類需要消耗較多的資源或者沒有多個實例的情況下。例如,一個公司只有一個CEO、一個應用只有一個Application對象等。下面以公司裏的CEO爲例來簡單演示一下,一個公司可以有幾個VP、無數個員工,但是CEO只有一個,代碼如下:

//普通員工
public class Staff {
	public void work(){
		//幹活
	}
}

//副總
public class VP extends Staff {
  	public void work() {
		//管理下面的經理
	}
}

//CEO,餓漢單例模式
public class CEO extends Staff {

	private static final CEO mCeo=new CEO();
	//構造函數私有
	private CEO(){
		
	}
	
	//公有的靜態函數,對外暴露獲取單例對象的接口
	public static CEO getCeo(){
		return mCeo;
	}

	public void work() {
		//管理VP
	}
}
//公司類
public class Company {

	private List<Staff> allStaff=new ArrayList();
	public void addStaff(Staff per){
		allStaff.add(per);
	}
	public void showAllStaffs(){
		for (Staff per:allStaff) {
			System.out.println("Obj:"+per.toString());
			
		}
	}
}

測試類

public class Test {
	public static void main(String[] args) {
		Company cp = new Company();
		// CEO對象只能通過getCeo函數獲取
		Staff ceo1 = CEO.getCeo();
		Staff ceo2 = CEO.getCeo();

		cp.addStaff(ceo1);
		cp.addStaff(ceo2);

		// 通過new 創建VP對象
		Staff vp1 = new VP();
		Staff vp2 = new VP();
		// 通過new創建Staff對象
		Staff staff1 = new Staff();
		Staff staff2 = new Staff();

		cp.addStaff(vp1);
		cp.addStaff(vp2);
		cp.addStaff(staff1);
		cp.addStaff(staff2);
		cp.showAllStaffs();
	}
}

 

輸出結果如下:

Obj:Singleton.CEO@15db9742
Obj:Singleton.CEO@15db9742
Obj:Singleton.VP@6d06d69c
Obj:Singleton.VP@7852e922
Obj:Singleton.Staff@4e25154f
Obj:Singleton.Staff@70dea4e

從上述的代碼中可以看出,CEO類不能通過new的形式構造對象,只能通過CEO.getCEO()函數來獲取,而這個CEO對象是靜態函數,並且在聲明的時候已經初始化了,這就保證了CEO對象的唯一性。從輸出結果中發現,CEO兩次輸出的CEO對象都是一樣的,而VP、Staff等類型的對象都是不同的。這個實現的核心在於 將CEO類的構造方法私有化,使得外部程序不能通過構造函數來構造CEO對象,而CEO類通過一個靜態方法返回一個靜態對象。

1 懶漢模式

懶漢模式是聲明一個靜態對象,並且在用戶第一次調用getInstance時進行初始化,而上述的惡漢模式是在聲明靜態對象時就已經初始化。懶漢單例模式實現如下:

public class Singleton {
	private static Singleton instance;

	private Singleton() {
	}

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

getInstance()方法中添加synchronized關鍵字,也就是getInstance()是一個同步方法,這就是上面所說的在多線程情況下保證單例對象唯一性的手段。細想一下,就會發現一個問題,即使instance已經被初始化(第一次調用時就會初始化instance),每次調用getInstance()方法都會進行同步,這樣會不必要的資源,這也是懶漢單例模式存在的最大問題。
最後總結一下,懶漢單例模式的有點是單例只有在使用時纔會被實例化,在一定程度上節約了資源;缺點是第一次加載時需要及時進行實例化,反應稍慢,最大問題是每次調用getInstance()都進行同步,造成不必要的同步開銷。 這種模式一般不建議使用。

  1. Double Check Lock(DCL)實現單例

DCL方式實現單例模式的優點是既能夠在需要時才初始化單例,又能夠保證線程安全,且單例對象初始化後調用getInstance()不能進行同步鎖。代碼如下所示:


public class Singleton {
	private static Singleton instance = null;

	private Singleton() {
	}

	public void doSomething() {
		System.out.println("do sth.");
	}

	public static Singleton getInSingleton() {
		if (instance == null) {
			synchronized (Singleton.class) {
				if (instance == null) {
					instance = new Singleton();

				}
			}
		}
		return instance;
	}
}

本程序的亮點自然都在getInstance方法上,可以看到getInstance方法中對instance進行了兩次判空:第一層判斷主要是爲了避免不必要的同步,第二層的判斷則是爲了在null的情況下創建實例。這是什麼意思呢?下面具體分析:
假設線程A執行到sInstance=new Singleton()語句,這裏看起來是一句代碼,但實際上它並不是一個原子操作,這句代碼最終會被編譯成多條彙編指令,它大致做了3件事情:
1.給Singleton的實例分配內存;
2.調用Singleton()的構造函數,初始化成員字段;
3.將sInstance對象指向分配的內存空間(此時sInstance就不是 null了)。
但是由於Java編譯器允許處理器亂序執行,以及JDK1.5之前JMM中Cache、寄存器到主內存回寫順序的規定,上面的第二和第三的順序是無法保證的,也就是說,執行順序可能是1-2-3也可能是1-3-2.如果是後者,並且在3執行完畢後、2未執行之前,被切換到線程B上,這時候sInstance因爲已經在線程A執行過第三點,sInstance已經是非空了,所以,線程B直接取走sInstance,在使用是就會報錯,這就是DCL失效問題,而且這種難以跟蹤難以重現的錯誤很可能會隱藏很久。
在JDK1.5之後,調整了JVM,具體化了volatile關鍵字,因此如果在JDK是1.5或之後的版本,只需要將sInstance的定義改爲 private volatile static Singleton sInstance=null 就可以保證sInstance對象每次都是從主內存中讀取,就可以使用DCL的寫法來完成單例模式。當然,volatile或多或少也會影響到性能,但考慮到程序的正確性,犧牲這點性能還是值得的。
DCL的優點:資源利用率高,第一次執行getInstace時單例對象纔會被實例化,效率高。缺點:第一次加載時反應稍慢,也由於Java內存模型的原因偶爾會失敗。在高併發環境下也有一定的缺陷,雖然發生概率很小。DCL模式是使用最多的單例實現方式,它能夠在需要時纔是實例化單例對象,並且能夠在絕大多數場景下保證單例對象的唯一性。

3.靜態內部類單例模式

DCL雖然在一定程度上解決了資源消耗、多餘的同步、線程安全等問題,但是,它還是在某些情況下出現失效的問題。建議使用如下的代碼替代:

public class Singleton2 {
	
	public static Singleton2 getInSingleton2() {
		return SingletonHolder.instance;
	}

	/***
	 * 靜態內部類
	 */
	private static class SingletonHolder {
		private static final Singleton2 instance = new Singleton2();
	}
}

當第一次加載Singleton類時並不會初始化sInstance,只有在第一次調用Singleton的getInstance方法時纔會導致sInstance被初始化。因此,第一次調用getInstance方法會導致虛擬機加載SingletonHolder類,這種方式不僅能夠確保線程安全,也能夠保證代理對象的唯一性,同時也延遲了單例的實例化,所以這是推薦使用的單例模式實現方式。

6.總結

單例模式是運用頻率很高的模式,但是,由於在客戶端通常沒有高併發的情況,因此,選擇哪種實現方式並不會有太大的影響,即便如此,出於效率考慮DCL方式和靜態內部類方式。
優點:
1.由於單例模式在內存中只有一個實例,減少了內存的開支,特別是一個對象需要頻繁地創建、銷燬時,而且創建或銷燬時性能又無法優化,單例模式的優勢就非常明顯。
2.由於單例模式只生成一個實例,所以,減少了系統的性能開銷,當一個對象的產生需要比較多的資源時,如讀取配置、生產其它依賴對象時,則可以通過在應用啓動時直接產生一個單例對象,然後用永久駐留內存的方式來解決。
3.單例模式可以避免對資源的多重佔用,例如一個寫文件操作,由於只有一個實例存在內存中,避免了對同一個資源文件的同時寫操作。
4.單例模式可以在系統設置全局的訪問點,優化和共享資源訪問,例如,可以設計一個單例類,負責所有數據表的映射處理。
缺點:
1.單例模式一般沒有接口,擴展很困難,若要擴展,除了修改代碼基本上沒有第二種途徑可以實現。
2.單例對象如果持有Context,那麼很容易引起內存泄漏,此時需要注意傳遞給單例的Context最好是Application Context。

———摘自《Android 源碼設計模式解析與實戰 第二章》

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