菜鳥成長系列-單例模式

glmapper發表於2017-12-17

菜鳥成長系列-概述
菜鳥成長系列-物件導向的四大基礎特性
菜鳥成長系列-多型、介面和抽象類
菜鳥成長系列-物件導向的6種設計原則

前面已經將設計模式中的基本內容擼了一下,今天開始正式開始設計模式系列的內容,因為網上也有很多關於設計模式的技術部落格,從不同的角度對設計模式都做了很詳細的解讀;本系列的模式除了基本的概念和模型之外,還會結合java自身使用的和Spring中使用的一些案例來進行學習分析。
水平有限,如果存在不當之處,希望大家多提意見,灰常感謝!
設計模式中總體分為三類:
一、建立型(5):

  • 工廠方法[Factory Method]
  • 抽象工廠[Abstract Factory]
  • 原型[Prototype]
  • 建造者[Builder]
  • 單例[Singleton]

還有一個簡單工廠[Simple Factory],目前有兩種,有的把單例模式作為這5種之一,有的是將簡單工廠作為這5種之一。這裡不做討論,原則上兩個都是,只是劃分規則不同。

二、結構型(7)

  • 介面卡[Adapter]
  • 橋接[Bridge]
  • 組合[Composite]
  • 裝飾[Decorator]
  • 外觀[Facade]
  • 享元[Flyweight]
  • 代理[Proxy]

三、行為型(11)

  • 策略[Strategy]
  • 模板方法[Template method]
  • 職責鏈[Chain of Responsibility]
  • 迭代器[Iterator]
  • 狀態[State]
  • 訪問者[Visitor]
  • 命令[Command]
  • 備忘錄[Memento]
  • 觀察者[Observer]
  • 中介者[Mediator]
  • 直譯器[Interpreter]

單例模式

首先它是一種建立型模式,與其他模式區別在於:單例模式確保被建立的類只有一個例項物件,而且自行例項化並向整個系統提供這個例項。一般情況下我們稱當前這個類為單例類。

從上面這段話中我們可以瞭解到,單例模式具備以下三個要點:

  • 某個類只能有一個例項
  • 必須自行建立這個例項[具體的物件建立由類本身負責,其他類不負責當前類的建立]
  • 必須向整個系統提供這個例項[也就是說,當前類需要對外提供一個獲取當前例項的一個方法,且該方法不能是私有的]

OK,來看單例模式的幾種實現方式。

方式一:餓漢式

package com.glmapper.design.singleton;
/**
 * 單例模式-餓漢式
 * @author glmapper
 * @date 2017年12月17日下午10:30:38
 */
public class EagerSingleton {
	/**
	 * 內部直接提供一個eagerSingletonInstance;
	 * 我們知道,一般情況下,如果一個變數被static final修飾了,那麼該變數將會被視為常量。
	 * 滿足要點:自行建立
	 */
	private static final EagerSingleton eagerSingletonInstance = new EagerSingleton();
	/**
	 * 提供一個私有的建構函式,這樣其他類就無法通過new
	 * EagerSingleton()來獲取物件了,同樣也保證了當前類不可以被繼承
	 * 滿足要點:某個類只能有一個例項
	 */
	private EagerSingleton(){}
	/**
	 * 對外提供一個獲取例項的方法
	 * 滿足要點:向整個系統提供這個例項
	 */
	public static EagerSingleton getInstance(){
		return eagerSingletonInstance;
	}
}
複製程式碼

方式二:懶漢式

package com.glmapper.design.singleton;
/**
 * 單例模式-懶漢式
 * @author glmapper
 * @date 2017年12月17日下午10:45:54
 */
public class LazySingleton {
	//提供一個私有靜態變數,注意區別與餓漢式中的static final。
	private static LazySingleton lazySingletonInstance = null ;
	//同樣需要提供一個私有的構造方法,其作用與餓漢式中的作用一樣
	private LazySingleton(){}
	/**
	 * 1.使用synchronized來保證執行緒同步
	 * 2.例項的具體建立被延遲到第一次呼叫getInstance方法時來進行
	 * 3.如果當前例項已經存在,不再重複建立
	 */
	public synchronized static LazySingleton getInstance(){
		if (lazySingletonInstance == null) {
			lazySingletonInstance = new LazySingleton();
		}
		return lazySingletonInstance;
	}
}

複製程式碼

餓漢式單例類在自己被載入時就自己例項化了,即便載入器是靜態的,在餓漢式單例類被載入時仍會將自己例項化。從資源利用角度來說,這個比懶漢式單例類稍微的差一些。如果從速度和響應時間來看,餓漢式就會比懶漢式好一些。懶漢式在單例類進行例項化時,必須處理好在多個執行緒同時首次引用此類時的訪問限制問題。

方式三:登記式

package com.glmapper.design.singleton;
import java.util.HashMap;
/**
 * 單例模式-登記式
 * @author glmapper
 * @date 2017年12月17日下午10:58:36
 */
public class RegisterSingleton {
	//提供一個私有的HashMap型別的registerSingletonInstance儲存該RegisterSingleton型別的單例
	private static HashMap<String,Object> registerSingletonInstance = new HashMap<>();
	//通過static靜態程式碼塊來進行初始化RegisterSingleton當前類的例項,並將當前例項存入registerSingletonInstance
	static {
		RegisterSingleton singleton = new RegisterSingleton();
		registerSingletonInstance.put(singleton.getClass().getName(), singleton);
	}
	/**
	 * 注意區別,此處提供的是非private型別的,說明當前類可以被繼承
	 */
	protected RegisterSingleton(){}
	/**
	 * 獲取例項的方法
	 */
	public static RegisterSingleton getInstance(String name){
		//如果name為空,則那麼預設為當前類的全限定名
		if (name == null) {
			name ="com.glmapper.design.singleton.RegisterSingleton";
		}
		//如果map中沒有查詢到指定的單例,則將通過Class.forName(name)來建立一個例項物件,並存入map中
		if (registerSingletonInstance.get(name)==null) {
			try {
				registerSingletonInstance.put(name, Class.forName(name).newInstance());
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
		//返回例項
		return (RegisterSingleton) registerSingletonInstance.get(name);
	}
}
複製程式碼

登記式單例是Gof為了克服餓漢式和懶漢式單例類均不可被繼承的缺點而設計的。

package com.glmapper.design.singleton;
/**
 * 登記式-單例-子類
 * @author glmapper
 * @date 2017年12月17日下午11:14:03
 *
 */
public class ChildRegisterSingleton extends RegisterSingleton
{
	/**
	 * 由於子類必須允許父類以構造方法呼叫產生例項,因此,子類的構造方法必須
	 * 是public型別的。但是這樣一來,就等於說可以允許以new 
	 * ChildRegisterSingleton()的方式產生例項,而不必在父類的登記中。
	 */
	public ChildRegisterSingleton(){}	
	
	//客戶端測試獲取例項
	public static void main(String[] args) {
		ChildRegisterSingleton crs1 = (ChildRegisterSingleton) getInstance(
				"com.glmapper.design.singleton.ChildRegisterSingleton");
		ChildRegisterSingleton crs2 = (ChildRegisterSingleton) getInstance(
				"com.glmapper.design.singleton.ChildRegisterSingleton");
		System.out.println(crs1 == crs2);
	}
}

返回:true   這個同志們可以自行驗證,肯定是一樣的。但是不能使用new,
因為前提約束是,需在父類中登記的才是單例。
複製程式碼

方式四:雙重檢測模式,雙重檢測方式在某些書上或者文獻中說對於java語言來說是不成立的,但是目前確實是通過某種技巧完成了在java中使用雙重檢測機制的單例模式的實現,;這種技巧後面來說;關於為什麼java語言對於雙重檢測成例不成立,大家可以在[BLOCH01]文獻中看下具體情況。
先來看一個單執行緒模式下的情況:

package com.glmapper.design.singleton;
/**
 * 一個錯誤的單例例子
 * @author glmapper
 * @date 2017年12月17日下午11:53:04
 */
public class DoubleCheckSingleton {
	private static DoubleCheckSingleton instance=null;
	public static DoubleCheckSingleton getDoubleCheckSingleton(){
		if (instance == null) {
			instance = new DoubleCheckSingleton();
		}
		return instance;
	}
}

複製程式碼

這個很明顯是一個錯誤的例子,對於A/B兩個執行緒,因為step 1並沒有使用同步策略,因此執行緒A/B可能會同時進行// step 2,這樣的話,就會可能建立兩個物件。那麼正確的方式如下:使用synchronized關鍵字來保證同步。

package com.glmapper.design.singleton;
/**
 * 這是一個正確的開啟方式哦。。。
 * @author glmapper
 * @date 2017年12月17日下午11:53:04
 */
public class DoubleCheckSingleton {
	private static DoubleCheckSingleton instance=null;
	//使用synchronized來保證getDoubleCheckSingleton同一時刻只能被一個執行緒訪問
	public synchronized static DoubleCheckSingleton getDoubleCheckSingleton(){
		if (instance == null) {
			instance = new DoubleCheckSingleton();
		}
		return instance;
	}
}
複製程式碼

這種方式雖然保證了執行緒安全性,但是也存在另外一種問題:同步化操作僅僅在instance首次初始化操作之前會起到作用,如果instance已經完成了初始化,對於getDoubleCheckSingleton每一次呼叫來說都會阻塞其他執行緒,造成一個不必要的瓶頸。那我們就通過使用更加細粒度化的鎖,來適當的減小額外的開銷。OK,下面再來一個錯誤的例子:

package com.glmapper.design.singleton;
/**
 * 一個錯誤的單例例子
 * @author glmapper
 * @date 2017年12月17日下午11:53:04
 */
public class DoubleCheckSingleton {
	private static DoubleCheckSingleton instance=null;
	//使用synchronized來保證getDoubleCheckSingleton同一時刻只能被一個執行緒訪問
	public static DoubleCheckSingleton getDoubleCheckSingleton(){
		if (instance == null) {  //1
		    // B執行緒檢測到uniqueInstance不為空
			synchronized (DoubleCheckSingleton.class) { //2
				if (instance == null) { //3
					instance = new DoubleCheckSingleton();//4
					// A執行緒被指令重排了,剛好先賦值了;但還沒執行完建構函式。
				}
			}
		}
		// 後面B執行緒執行時將引發:物件尚未初始化錯誤。
		return instance;//5
	}
}
複製程式碼

看起來沒什麼毛病呀?我們來分析,兩個執行緒A和B,同時到達1,且都通過了1的檢測。此時A到了4,B在2。此時B執行緒檢測到instance不為空,A執行緒被指令重排了,剛好先賦值了;但還沒執行完建構函式;再接下來B執行緒執行時將引發:物件尚未初始化錯誤(5)。

對於上面的問題,我們可以通過volatile關鍵字來修飾instance物件,來保證instance物件的記憶體可見性和防止指令重排序。這個也就是前面說到的“技巧”。

private static DoubleCheckSingleton instance=null;
改為:
private static volatile DoubleCheckSingleton instance=null;
複製程式碼

本篇將單例模式的幾種情況進行了分析。後面將會對將java中和Spring中所使用的單例場景進行具體的案例分析。

JAVA中的單例模式使用

JAVA中對於單例模式的使用最經典的就是RunTime這個類。

菜鳥成長系列-單例模式
菜鳥成長系列-單例模式
註釋解讀:每個Java應用程式都有一個Runtime類的單個例項,允許應用程式與執行應用程式的環境進行互動。 當前執行時可以從getRuntime方法獲得。應用程式不能建立它自己的這個類的例項。

看過上篇文章的小夥伴可能比較清楚,這裡RunTime使用的是懶漢式單例的方式來建立的。Runtime提供了一個靜態工廠方法getRuntime方法用於獲取Runtime例項。Runtime這個類的具體原始碼分析和只能此處不做分析。

Spring中的單例

Spring依賴注入Bean例項預設是單例的。Spring中bean的依賴注入都是依賴AbstractBeanFactory的getBean方法來完成的。那我們就來看看在getBean中都發生了什麼。

org.springframework.beans.factory.suppor.AbstractBeanFactory

菜鳥成長系列-單例模式
從上面這張圖中我們啥也看不出,只知道在getBean中又呼叫了doGetBean方法(Spring中還有java原始碼中有很多類似的寫法,好處在於我們可以通過子類繼承,繼而編寫我們自己的處理邏輯)。OK,再來看看doGetBean方法。

菜鳥成長系列-單例模式
來看下這個方法的註釋:返回指定的bean可以共享或獨立的例項 (谷歌+有道+百度)

  • name:要檢索的bean的名稱
  • requiredType:要檢索的bean所需的型別
  • args:如果使用靜態工廠方法的顯式引數建立原型,則使用引數。 在其他情況下使用非空args值是無效的。
  • typeCheckOnly:獲得例項是否是為了型別檢查,而不是實際的使用

這個方法體內的程式碼非常的多,那麼我們本文不是來學習Spring的,所以我們只看我們關心的部分,

菜鳥成長系列-單例模式
為手工註冊的singleton檢查單例快取。,從這個註釋可以看出,此處就是我們獲取例項的地方,再往下看。

此處和上面的getBean一樣,也是通過模板方法的方式進行呼叫的。

菜鳥成長系列-單例模式
OK,這裡我們看到了獲取單例例項的具體實現過程。 返回註冊在給定名稱下的(原始的)singleton物件。檢查已經例項化的單例,並且還允許提前引用當前建立的單例(解析迴圈引用)。
菜鳥成長系列-單例模式
這裡使用的是餓漢式中的雙重檢測機制來實現的。

OK,至此單例模式的學習就結束了,下一篇文章將會介紹工廠模式(簡單工廠,工廠方法,抽象工廠)。

相關文章