引言
今天來談談設計模式中的單例模式,溫故知新,以免生疏,
軟體設計領域的四位世界級大師Gang Of Four (GoF):Erich Gamma,Richard Helm,Ralph Johnson,John Vlissides四人合著了《Design Patterns - Elements of Reusable Object-Oriented Software》一書,(中文譯名:《設計模式:可復用面向物件軟體的基礎》),該書首次提到了軟體開發中設計模式的概念,對面向物件軟體設計產生了巨大影響,
- 創建型模式
單例模式屬于創建型模式,那么這里就要簡述一下創建型模式,顧名思義,就是創建物件的設計模式,頻繁地使用基本物件創建方式(比如new操作)會使系統的耦合性變高,導致某些設計上的問題,創建型模式對類的實體化進行抽象,將物件的創建與物件的使用分離,隱藏了類的實體化程序,
- 單例模式的由來
在系統中,有一些物件其實只需要一個,例如執行緒池、快取、注冊表、日志物件、充當列印機顯卡等設備驅動程式的物件,同時這些類比較龐大復雜,并且這些物件完全可以復用,若創建多個實體或頻繁創建銷毀實體物件,會導致程式行為例外、資源使用過量、或者不一致性的結果,這就有了單例模式,
-
單例模式含義
確保一個類只有一個實體,并提供該實體的全域訪問點,后半句通俗點講,就是向整個系統提供這個實體,
類的構成
-
建構式:
private Singleton(){},私有,因為一個類只能有一個實體,不可被外部再次實體化,構造方法不可能是public,只能是private,保證了不能通過構造器進行創建實體物件, -
(成員)變數:
private static Singleton instance,私有,靜態變數,由于類中僅有一個實體,屬于當前類的靜態變數,外部無法直接訪問, -
方法:
public static Singleton getInstance(){},公有,靜態方法,要向整個系統提供該單例,就要創建一個公有的靜態方法向外界提供當前類的實體,
實作方式
1. 懶漢式(執行緒不安全)
- 描述:這是最基本的實作方式,實體物件在第一次被呼叫的時候才會被創建,屬于懶加載,延遲創建單例,
- 優點:懶加載模式下,如果沒有使用到該類,那么就不會實體化物件,節約了資源,
- 缺點:這種實作方式不支持多執行緒,這是最主要的問題,因為沒有加鎖 synchronized,無法實作執行緒安全,在執行
if (Instance == null)時,如果多個執行緒同時進入,并且此時Instance為 null,那么這些執行緒就會在執行new陳述句,導致多次實體化物件,這是我們不希望看到的,
public class Singleton {
private static Singleton instance; //宣告靜態變數
private Singleton (){} //構造器
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
2. 懶漢式(執行緒安全)
- 描述:實體物件在也是第一次被呼叫的時候才會被創建,屬于懶加載,通過靜態同步方法解決執行緒安全問題,
- 優點:懶加載,節約記憶體資源,使用同步方法,在某個時間點只能有一個執行緒能夠進入方法,避免了多次實體化的問題,因此支持多執行緒,保證了執行緒安全,
- 缺點:我們希望在創建實體的時間點進行加鎖同步,用靜態同步方法會使得同步的范圍太大,另外每次要創建物件都要爭搶鎖,未進入方法的執行緒必須等待,性能會有損耗,效率不高,
public class Singleton {
private static Singleton instance;
private Singleton (){}
public static synchronized Singleton getInstance() { //使用同步方法
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
3. 餓漢式(執行緒安全)
- 描述:執行緒不安全問題主要是由于
Instance可被多次實體化,因此,在類加載時就直接實體化Instance就可以保證執行緒安全問題,它基于 累加載機制避免了多執行緒的同步問題,不過這時候初始化instance顯然沒有達到懶加載(lazy loading)的效果, - 優點:未使用同步鎖,執行效率會有所提高,執行緒安全,
- 缺點:直接在類加載時自動實體化物件,失去了懶加載機制下節約資源的優勢,消耗記憶體,
public class Singleton {
private static Singleton instance = new Singleton(); //類加載時就進行實體化
private Singleton (){}
public static Singleton getInstance() {
return instance;
}
}
注:懶漢式與餓漢式最主要的區別在于創建單例的時機不同,懶漢式根據是否需要實體,手動創建;餓漢式在類加載時自動創建單例
4. 雙重校驗鎖方式(執行緒安全)
-
描述:雙重校驗鎖(double-checked locking,DCL)也叫雙檢鎖,JDK1.5出現的功能,這種方式采用雙鎖機制,同時加鎖操作只需要對實體化那部分代碼進行,只有當
Instance沒有被實體化時(Instance == null) ,才需要進行加鎖, -
優點:懶加載,多執行緒環境下可保證執行緒安全,性能較高,
-
缺點:相比前幾種方式,實作較為復雜,
-
雙重校驗鎖的完善程序:
- 由于使用懶漢式同步方法會消耗過多性能,我們只在構建實體物件的時候進行同步,在呼叫
getInstance()時,訪問的執行緒不需要競爭鎖,都可以直接進入,再進行下一步判斷,若此時實體物件還沒有被構建,執行緒開始競爭鎖,搶到鎖的執行緒開始創建單例,
public class Singleton {
private static Singleton Instance;
private Singleton() {}
public static Singleton getInstance() {
if (Instance == null) {
synchronized (Singleton.class) { //在需要構建單例的時候給Class物件加鎖
Instance = new Singleton();
}
}
return Instance;
}
}
問題:在多個執行緒執行判斷條件時,雖然只有一個執行緒能夠搶到鎖取創建單例,但是可能有其他執行緒已經進入了if代碼塊,之后會再進行if判斷了,而這些執行緒等待釋放鎖后,隨即又會創建實體物件,最終實體會被多次被創建,顯然執行緒不安全,
- 再增加一條判斷條件,這也是雙重校驗鎖中“雙重”的由來,我們假設執行緒A搶到同步鎖,然后創建實體,創建完畢釋放鎖,這時,執行緒B搶到鎖,進行判斷實體是否被創建,發現實體
instance已經被執行緒A初始化了,不可能等于null,直接退出,回傳A執行緒創建的單例,
public class Singleton {
private static Singleton Instance;
private Singleton() {}
public static Singleton getInstance() {
if (Instance == null) {
synchronized (Singleton.class) { //在需要構建單例的時候給Class物件加鎖
if (Instance == null) { //增加了判空條件
Instance = new Singleton();
}
}
}
return Instance;
}
}
- 在執行
Instance = new Singleton();時,大致可以分為三步:1)給Instance實體分配記憶體;2)初始化Instance的構造器;3)將instance物件指向分配的記憶體地址(這一步Instance就非null了),由于JVM為了優化指令,提高程式的運行效率,允許指令重排,導致在程式實際運行的時候,順序變為1)>> 3)>> 2),這在單執行緒的情況下是沒有問題的,但是,在多執行緒的環境下,執行緒有可能拿到一個尚未被初始化的實體,程式必然報錯,使用 volatile 關鍵字可以禁止 JVM 的指令重排,保證在多執行緒環境下也能正常運行,
public class Singleton {
private volatile static Singleton Instance; //增加volatile關鍵字,防止JVM指令重排
private Singleton (){}
public static Singleton getInstance() {
if (Instance == null) {
synchronized (Singleton.class) {
if (Instance == null) {
Instance = new Singleton();
}
}
}
return singleton;
}
}
5.靜態內部類方式
- 描述:這種方式能達與雙檢鎖功能相似,且實作更簡單,由于靜態內部類的加載是在程式中呼叫靜態內部類的時候加載的,和外部類的加載沒有必然關系,因此當
Singleton類加載時,靜態內部類SingletonHolder并沒有被加載進記憶體,只有當呼叫getInstance()方法從而觸發SingletonHolder.INSTANCE時 ,SingletonHolder才會被加載,此時初始化INSTANCE實體,實作了延遲加載, 這種方式只適用于靜態域的情況,雙檢鎖方式可在實體域需要延遲初始化時使用, - 優點:延時加載,按需加載,節約資源;由于JVM提供了對執行緒安全的支持,只會加載一遍,執行緒安全得到保證,
- 缺點:這種方式只適用于靜態域的情況,
public class Singleton {
private Singleton() {
}
//靜態內部類
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingletonHolder.INSTANCE; //訪問靜態內部類中靜態成員
}
}
6.列舉方式
- 描述:這是實作單例模式的最佳方法,這種方式是《Effective Java》作者 Joshua Bloch 提倡的方式,它不僅能避免多執行緒同步問題,而且還自動支持序列化機制,防止反序列化重新創建新的物件,絕對防止多次實體化,出現反射攻擊時,通過
setAccessible()方法可以將私有建構式的訪問級別設定為public,然后呼叫建構式從而實體化物件,如果要防止這種攻擊,需要在建構式中添加防止實體化第二個物件的代碼,解決序列化和反射攻擊很麻煩,而列舉實作不會出現這兩種問題,因此說列舉實作單例模式式最佳實踐方法, - 優點:單例模式的最佳實踐,它實作簡單,并且在面對復雜的序列化或者反射攻擊的時候,不能呼叫
private方法,能夠防止多次實體化,是目前最安全的實作單例的方法, - 缺點:這種方式尚未被廣泛采用,實際作業中,很少會被采用,
public enum Singleton {
INSTANCE;
public void whateverMethod() { //任意方法
}
}
結語
一般情況下,不建議使用兩種懶漢式實作單例模式;明確使用靜態方法和實作懶加載效果時,會采用靜態內部類方式;涉及到反序列化創建物件的時候,可以使用列舉方式;一般而言,餓漢式以及雙重校驗鎖比較常用,
本文來自博客園,作者:Joe__Bryant,轉載請注明原文鏈接:https://www.cnblogs.com/KobeForever/p/WhereAmazingHappens1.html
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/500287.html
標籤:Java
