1. 程式人生 > >設計模式—單例模式(Singleton)

設計模式—單例模式(Singleton)

一、什麼是單例模式:

單例(Singleton)模式是一種常用的建立型設計模式。

簡單來說就是一個類只能構建一個物件的設計模式。

核心作用:保證一個類只有一個例項,並且提供一個訪問該例項的全域性訪問點。

二、單例模式的應用場景:

1、需要生成唯一序列的環境

2、需要頻繁例項化然後銷燬的物件。

3、建立物件時耗時過多或者耗資源過多,但又經常用到的物件。 

4、方便資源相互通訊的環境

舉個例子:

1、windows桌面上的回收站,當我們試圖再次開啟一個新的回收站時,Windows系統並不會為你彈出一個新的回收站視窗。

也就是說整個windows系統執行過程中只會維護一個回收站例項。

2、一般網站上統計實時線上人數的計數器也是單例模式。

三、單例模式的優缺點:

優點:

1、在記憶體裡只有一個例項,減少了記憶體的開銷,尤其是頻繁的建立和銷燬例項(比如管理學院首頁頁面快取)。

2、避免對資源的多重佔用(比如寫檔案操作)。

缺點:

1、不適用於變化的物件,如果同一型別的物件總是要在不同的用例場景發生變化,單例就會引起資料的錯誤,不能儲存彼此的狀態。 
2、由於單利模式中沒有抽象層,因此單例類的擴充套件有很大的困難。 
3、單例類的職責過重,在一定程度上違背了“單一職責原則”。

四、單例模式的實現:

單例模式大致的實現步驟:

1、私有建構函式,防止被例項化

2、持有私有靜態例項

3、公開靜態工廠方法,獲取唯一可用的物件

單例模式的幾種實現方式:

1、餓漢式

它是在類裝載時例項化物件,所以不支援懶載入,但它是執行緒安全的,也是平常使用較多的一種方式。

/** 單例模式-餓漢式-執行緒安全 */
public class Singleton {
	// 私有建構函式,防止被例項化
	private Singleton(){}	
	// 單例物件 類載入時建立instance 避免了多執行緒同步問題
	private static Singleton instance = new Singleton();	
	// 靜態工廠方法,獲取唯一可用的物件
	public static Singleton getInstance() {
		return instance;
	}
}

2、懶漢式

需要用時例項化物件,支援懶載入,非執行緒安全。因為沒有加鎖 synchronized,所以嚴格意義上它並不算單例模式。

/** 單例模式-懶漢式-非執行緒安全 */
public class Singleton {
	// 私有建構函式,防止被例項化
	private Singleton(){}	
	// 單例物件 此處賦值為null,目的是實現延遲載入
	private static Singleton instance = null;	
	// 靜態工廠方法,建立唯一可用的物件
	public static Singleton getInstance() {
		if (instance == null) {
			instance = new Singleton();
		}
		return instance;
	}
}

那麼如何讓他支援多執行緒,變成執行緒安全呢?  加鎖 synchronized

/** 單例模式-懶漢模式-執行緒安全 */
public class Singleton {
	// 私有建構函式,防止被例項化
	private Singleton(){};
	// 單例物件	此處賦值為null,目的是實現延遲載入
	private static Singleton instance = null;
	// 靜態工廠方法,建立唯一可用的物件
	public static synchronized Singleton getInstance(){
		if (instance == null) {
			instance = new Singleton();
		}
		return instance;
	}
}

這種方式具備很好的懶載入(lazy loading),能夠在多執行緒中很好的工作,但是,效率很低,99% 情況下不需要同步。

那麼繼續優化看第三種實現方式 ↓

3、雙檢鎖/雙重校驗鎖

這種方式採用雙鎖機制支援懶載入、執行緒安全且在多執行緒情況下能保持高效能。

/** 單例模式-雙檢鎖/雙重校驗鎖-執行緒安全 */
public class Singleton {
	// 私有建構函式,防止被例項化
	private Singleton(){};
	// 單例物件	此處賦值為null,目的是實現延遲載入
	private static Singleton instance = null;
	// 靜態工廠方法,建立唯一可用的物件
	public static Singleton getInstance(){
		// 雙檢鎖/雙重校驗鎖
		if (instance == null) {
			//同步鎖
			synchronized(Singleton.class){
				if (instance == null) {
					instance = new Singleton();
				}
			}
		}
		return instance;
	}
}

認真看的同學可能會發現其實這個Singleton 類雖然也加了鎖synchronized 但是並沒有解決多執行緒問題。

試想執行緒A走到第11行程式碼,Singleton 類第一次建立例項,同時執行緒B進來走到第8行程式碼。

這種情況下執行緒B第8行程式碼中 if (instance == null)就很有可能返回false,從而獲取到未初始化完成的 instance

為什麼 if (instance == null) 會有可能返回false 呢?  這裡就涉及到了JVM編譯器和CPU的指令重排

指令重排是什麼意思呢?比如java中簡單的一句 instance = new Singleton,會被編譯器編譯成如下JVM指令:

memory =allocate();    //1:分配物件的記憶體空間 

ctorInstance(memory);  //2:初始化物件 

instance =memory;     //3:設定instance指向剛分配的記憶體地址 

但是這些指令順序並非一成不變,有可能會經過JVM和CPU的優化,指令重排成下面的順序:

memory =allocate();    //1:分配物件的記憶體空間 

instance =memory;     //3:設定instance指向剛分配的記憶體地址 

ctorInstance(memory);  //2:初始化物件 

當執行緒A執行完1,3時,instance物件還未完成初始化,但已經不再指向null。此時如果執行緒B搶佔到CPU資源,執行  if(instance == null)的結果會是false,從而返回一個沒有初始化完成的instance物件

如何避免這一情況呢?我們需要在instance物件前面增加一個修飾符volatile。

/** 單例模式-雙檢鎖/雙重校驗鎖-執行緒安全 */
public class Singleton {
	// 私有建構函式,防止被例項化
	private Singleton(){};
	// 單例物件	此處賦值為null,目的是實現延遲載入
	// 新增 volatile 是為了操作此物件時防止JVM和CPU指令重排
	private volatile static Singleton instance = null;
	// 靜態工廠方法,建立唯一可用的物件
	public static Singleton getInstance(){
		// 雙檢鎖/雙重校驗鎖
		if (instance == null) {
			//同步鎖
			synchronized(Singleton.class){
				if (instance == null) {
					instance = new Singleton();
				}
			}
		}
		return instance;
	}
}

volatile修飾符阻止了變數訪問前後順序的指令重排,保證了指令的執行順序。

如此線上程B看來,instance物件的引用要麼指向null,要麼指向一個初始化完畢的Instance,而不會出現某個中間態,保證了安全。

5、登記式/靜態內部類

這種方式通過靜態內部類支援懶載入、執行緒安全且在多執行緒情況下能保持高效能。

/** 單例模式-登記式/靜態內部類-執行緒安全 */
public class Singleton {
	// 私有建構函式,防止被例項化
	private Singleton(){};
	// 此處使用一個內部類來維護單例
	private static class SingletonFatory{
		private static Singleton instance = new Singleton();
	}
	// 靜態工廠方法,獲取唯一例項物件
	public static Singleton getInstance(){
		return SingletonFatory.instance;
	}
	// 如果該物件被用於序列化,可以保證物件在序列化前後保持唯一性
	public Object readResolve() {
		return getInstance();
	}
}

instance 物件初始化的時機並不是在單例類Singleton被載入的時候,而是在呼叫getInstance方法,使得靜態內部類SingletonFatory 被載入的時候。因此這種實現方式是利用classloader的載入機制來實現懶載入,並保證構建單例的執行緒安全。程式碼中readResolve()方法在下面補充。
6、列舉

這種實現方式還沒有被廣泛採用,但這是實現單例模式的最佳方法。它更簡潔,自動支援序列化機制,絕對防止多次例項化(防止反射構造物件,反射構造物件會在下面做具體補充),不過不支援懶載入。

package test;
/** 單例模式-列舉-執行緒安全 */
public enum Singleton {
	INSTANCE;

	public void whateverMethod(){
		System.out.println("列舉型別實現單例模式!");
	}

        public static void main(String[] args) {
		Singleton.INSTANCE.whateverMethod();
	}
}

為了好理解點我加了個main方法實現呼叫,這樣一看可能還會有點蒙(大神跳過),下面我寫一種好理解點的。

package test;
/** 單例模式-列舉-執行緒安全 */
public class EnumSingleton {
	// 私有建構函式
	private EnumSingleton(){};
	public static EnumSingleton getInstance(){
            return Singleton.INSTANCE.getInstance();
        }
	//列舉-靜態常量,隱式地用static final修飾過
	private enum Singleton{
            INSTANCE;
            private EnumSingleton singleton;
            //JVM會保證此方法絕對只調用一次
            //列舉實際上是類,這裡是構造方法
            private Singleton(){
                singleton = new EnumSingleton();
            }
            public EnumSingleton getInstance(){
                return singleton;
            }
        }
}

下面做兩點補充:

1、如果該單例類需要序列化則需加 readResolve() 方法,來確保物件在序列化前後保持唯一性;

具體實現在上面第5種實現方式程式碼裡有增加readResolve() 方法。

2、反射構造物件:

以上第1-5種單例實現方式都有一個共同的問題:無法防止利用反射構造物件重複構建物件,下面我們在餓漢式單例模式的基礎上來實現一下反射構造物件。

程式碼可以簡單歸納為三個步驟:

1、獲得單例類的構造器。

2、把構造器設定為可訪問。

3、使用newInstance方法構造物件。

/** 單例模式-餓漢式-執行緒安全 */
public class Singleton3 {
	// 私有建構函式,防止被例項化
	private Singleton3(){}	
	// 單例物件 類載入時建立instance 避免了多執行緒同步問題,但容易產生垃圾物件
	private static Singleton3 instance = new Singleton3();	
	// 靜態工廠方法,獲取唯一可用的物件
	public static Singleton3 getInstance() {
		return instance;
	}
	
	public static void main(String[] args) throws Exception{
		//獲得構造器
		Constructor con = Singleton.class.getDeclaredConstructor();
		//設定為可訪問
		con.setAccessible(true);
		//構造兩個不同的物件
		Singleton singleton1 = (Singleton)con.newInstance();
		Singleton singleton2 = (Singleton)con.newInstance();
		//驗證是否是不同物件
		System.out.println(singleton1.equals(singleton2));
	}
}

執行結果:

false

最後為了確認這兩個物件是否真的是不同的物件,我們使用equals方法進行比較。毫無疑問,比較結果是false。

第6種實現單例模式的方法(列舉)可以有效防止反射構造物件。