菜鳥成長系列-單例模式

菜鳥成長系列-概述
菜鳥成長系列-面向對象的四大基礎特性
菜鳥成長系列-多態、接口和抽象類
菜鳥成長系列-面向對象的6種設計原則
java

前面已經將設計模式中的基本內容擼了一下,今天開始正式開始設計模式系列的內容,由於網上也有不少關於設計模式的技術博客,從不一樣的角度對設計模式都作了很詳細的解讀;本系列的模式除了基本的概念和模型以外,還會結合java自身使用的和Spring中使用的一些案例來進行學習分析。
水平有限,若是存在不當之處,但願你們多提意見,灰常感謝!
設計模式中整體分爲三類:
1、建立型(5):spring

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

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

2、結構型(7)緩存

  • 適配器[Adapter]
  • 橋接[Bridge]
  • 組合[Composite]
  • 裝飾[Decorator]
  • 外觀[Facade]
  • 享元[Flyweight]
  • 代理[Proxy]

3、行爲型(11)安全

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

單例模式

首先它是一種建立型模式,與其餘模式區別在於:單例模式確保被建立的類只有一個實例對象,並且自行實例化並向整個系統提供這個實例。通常狀況下咱們稱當前這個類爲單例類。bash

從上面這段話中咱們能夠了解到,單例模式具有如下三個要點:app

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

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;
	}
}
複製代碼

方式二:懶漢式post

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,至此單例模式的學習就結束了,下一篇文章將會介紹工廠模式(簡單工廠,工廠方法,抽象工廠)。

相關文章
相關標籤/搜索