菜鳥成長系列-概述
菜鳥成長系列-物件導向的四大基礎特性
菜鳥成長系列-多型、介面和抽象類
菜鳥成長系列-物件導向的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,至此單例模式的學習就結束了,下一篇文章將會介紹工廠模式(簡單工廠,工廠方法,抽象工廠)。