用來創建獨一無二的,是能有一個實例的對象的入場券。告訴你一個好消息,單例模式的類圖可以說是所有模式的類圖中最簡單的,事實上,它的類圖上只有一個類!但是,可不要興奮過頭,儘管從類設計的視角來說很簡單,但是實現上還是會遇到相當多的波折。所以,系好安全帶,出發了! <! more 介紹 定義 單例模式(S ...
用來創建獨一無二的,是能有一個實例的對象的入場券。告訴你一個好消息,單例模式的類圖可以說是所有模式的類圖中最簡單的,事實上,它的類圖上只有一個類!但是,可不要興奮過頭,儘管從類設計的視角來說很簡單,但是實現上還是會遇到相當多的波折。所以,系好安全帶,出發了!
介紹
定義
單例模式(Singleton Pattern):確保一個類只有一個實例,並提供一個全局訪問點。
常用情景
有些對象其實我們只需要一個,比如:windows的任務管理器,項目中的讀取配置文件的對象,資料庫連接池,spring中的bean預設也是單例,線程池(threadpool),緩存(cache),對話框,處理偏好設置和註冊表(registry)的對象,日誌對象,充當印表機,顯卡等設備的驅動程式的對象。事實上,這類對象只能有一個實例,如果製造出多個實例,就會導致許多問題產生,例如:程式的行為異常,資源使用過量,或者是不一致的結果。
要點
優點
- 單例模式存在一個全局訪問點,所以優化共用資源;
- 只生成一個實例,減少了開銷,對於一些需要頻繁創建和銷毀的對象,單例模式可以提高系統性能。
缺點
- 由於單例模式中沒有抽象層,因此擴展困難;
- 職責過重,在一定程度上違背了“但一職責原則”。
類圖
在java中實現單例模式需要註意以下三點:
- 私有的構造器;
- 一個靜態方法;
- 一個靜態變數。
單例模式的實現
餓漢式
代碼實現
/** 通過餓漢式創建單例模式
* 當前類只能創建一個對象
* 天然線程安全(類只要被載入,就會被載入到全局變數中。所以餓漢式就是及時載入,不能實現懶載入)
*/
public class SingleHungry {
//提供一個靜態的全局變數作為訪問該實例的入口
private static SingleHungry instance = new SingleHungry();
/**
* 構造器私有,不讓外部通過new創建實例
*/
private SingleHungry() {}
/**
* 對外提供靜態的方法,用來獲取該類的實例
*/
public static SingleHungry getInstance() {
return instance;
}
}
要點
優點:線程安全;
缺點:不能懶載入。
懶漢式
代碼實現
/**
* 懶漢式,完成單例模式:
* 靜態的全局變數,初始化放在了靜態方法中,延遲產生了實例
* 延遲載入
* 線程不安全
*/
public class Singlelazy {
//提供一個靜態的全局變數作為訪問該實例的入口,但不立即載入
private static Singlelazy instance = null;
/**
* 構造器私有
*/
private Singlelazy() {
System.out.println("構造器被調用了");
}
/**
* 提供方法獲取該類的實例
* @return
*/
public static Singlelazy getInstance() {
//先查看是否存在對象,不存在則創建
if(instance == null) {
instance = new Singlelazy();
}
return instance;
}
}
以上代碼存在的問題如圖:
這段代碼是線程不安全的,當有多個線程同時訪問該代碼時,會出現創建多個實例的情況。
為了方便理解,我以Head First設計模式中的圖來介紹:
以上模擬了多線程問題的運行步驟,只是對象的名稱不一樣而已,相信對你來說是沒問題的。
解決思路1:
加鎖。鎖方法
問題:可以解決問題,但鎖住整個方法的粒度太大了,效率較低。不推薦使用
解決思路2:
加鎖。鎖代碼塊,先判斷後鎖
問題:不能解決問題,以上方式鎖的粒度變小了,但是並不能產生一個實例。原因:多個線程判斷之後全局變數都是null,進入後都開始等鎖。線程1出去,線程2進來繼續實例化,所以得到的對象是多個。
解決思路3:
加鎖。鎖代碼塊,先鎖後判斷。
以上方式解決了問題,當有多個線程同時訪問getInstance()。保證了多個線程是以流式,次序性的進入當前方法來獲取該類的實例。那麼效率一樣很低。而且多個線程同時等待。
上面的解決方法似乎都不怎麼好,那麼有沒有一種更好的方法來解決懶漢式的多線程安全問題呢?請繼續探索... ...
雙重檢測(雙重校驗鎖)
代碼實現
public class Singlelazy {
/*
* 雙重檢測(雙重校驗鎖)
* 延遲載入
* 線程安全
*/
private static volatile Singlelazy instance;
/*
* 構造器私有
*/
private Singlelazy() {
System.out.println("構造器被調用了");
}
public static Singlelazy getInstance() {
if(instance == null) {
synchronized (Singlelazy.class) {
if(instance == null) {
instance = new Singlelazy();
}
}
}
return instance;
}
}
要點
volatile關鍵字確保:當instance變數被初始化成Singleton實例時,多個線程正確地處理instance變數。
值得註意的是:很不幸地,在jdk1.5之前的版本,許多JVM對於volatile關鍵字的實現會導致dcl(double check locking)失效。如果你不能使用jdk1.4之後的版本,就請不要利用此技巧實現單例模式。
雙重檢測很好的解決了懶漢式多線程安全問題,可以和之前的幾種解決思路對比一下,思考一下優點在哪裡!
解決思路:
我們知道為瞭解決這個線程安全問題,必須加鎖,並且必須先鎖後判斷,思路3已經解決了問題,為了進一步優化代碼執行效率,我們再來改進一下代碼:
我們在鎖的外面再加一層對全局變數的判斷,這麼做的效果是什麼呢?當有多個線程來訪問時,例如:
- 線程1,線程2同時進入該方法;
- 經過最外面的判斷後,發現還沒有創建實例,這時就會依次執行鎖里的邏輯,並創建了實例;
- 假如這時線程3進入該方法,執行最外面的判斷,發現已經創建了實例,這時候直接返回就可以了,並不需要等鎖。
靜態內部類(餓漢式的變種)
代碼實現
/*
* 靜態內部類創建單例(利用類載入機制,保證線程安全問題)
* 線程安全
* 懶載入
*/
public class SingleInner {
private SingleInner() {}
private static class SingleInnerHolder{
private static SingleInner instance = new SingleInner();
}
public static SingleInner getInsstance() {
return SingleInnerHolder.instance;
}
}
要點
和餓漢式一樣採用的是classLoader機制,保證了線程安全問題,但不同的是,靜態內部類同樣滿足懶載入(當調用getInsstance()方法時,實例才會被創建)。
枚舉實現
代碼實現
public enum SingleEnum {
//定義示例化的單例對象
INSTANCE;
/*
* 對象執行的功能
*/
public void getInstance() {
}
}
要點
枚舉類的單例模式,不存在出現序列化,反射構建對象的漏洞(前面幾種單例實現方式都存在此問題)。不能懶載入。預設情況枚舉實例的創建都是線程安全,但是實例對象實現的方法,需要自己保證線程安全問題。
反射,序列化與單例的關係
反射
我們以餓漢式為例,如下圖:
通過反射獲取的s3和通過餓漢式獲取的s1,s2,並不是同一個實例,那麼有什麼辦法可以解決反射對單例的影響呢!請看下麵:
解決方法:
我們通過改變餓漢式構造方法的方式,來解決這個問題:
調用時拋出異常:
序列化
依舊以餓漢式為例,如下圖:
通過序列化,反序列化獲取的s3和通過餓漢式獲取的s1,s2,並不是同一個實例,那麼有什麼辦法可以解決序列化,反序列化對單例的影響呢!請看下麵:
通過在餓漢式中加入此方法,就可以解決這個問題:
創建的構建單例模式的方式比較
- 餓漢式 效率較高 不能懶載入 線程安全 調用率高
- 懶漢式 效率較低 懶載入 線程不安全
- 雙重檢測(雙重校驗鎖)懶漢式的一個變種 效率較高 懶載入 線程安全
- 靜態內部類 餓漢式的變種 效率較高 懶載入 線程安全
- 枚舉 效率較高 線程安全 不能懶載入
用法總結
- 懶漢式效率最低;
- 占用資源較少 不需要懶載入 枚舉優先 餓漢式;
占用資源較多 需要懶載入 靜態內部類優先 懶漢式(優先使用DCL)。
結語
設計模式源於生活