一個單例模式的晉級過程(餓漢-懶漢-DCL-IoDH-枚舉)

一個單例模式的晉級過程(餓漢-懶漢-DCL-IoDH-枚舉)

什麼是單例?

單例模式(Singleton Pattern)是 Java 中最簡單的設計模式之一。這種類型的設計模式屬於創建型模式,它提供了一種創建對象的最佳方式。

這種模式涉及到一個單一的類,該類負責創建自己的對象,同時確保只有單個對象被創建。這個類提供了一種訪問其唯一的對象的方式,可以直接訪問,不需要實例化該類的對象。

規則:

  • 單例類只能有一個實例。
  • 單例類必須自己創建自己的唯一實例。
  • 單例類必須給所有其他對象提供這一實例。

簡單來說就是一個特殊的類只能創建一次。

單例有哪些運用場景?

Windows的任務管理器,回收站,網站的計數器,線程池,數據庫連接池;

一下這些環境都可以考慮使用單例

1.需要生成唯一序列的環境
2.需要頻繁實例化然後銷燬的對象
3.創建對象時耗時過多或者耗資源過多,但又經常用到的對象
4.方便資源相互通信的環境

實現

1.餓漢式

**是否 Lazy 初始化:**否

**是否多線程安全:**是

**實現難度:**易

**描述:**這是最簡單的實現方式。

public class Singleton {  
    //內部調用構造函數創建的一個對象
    private static Singleton instance = new Singleton();  
    //讓構造函數爲 private,這樣該類就不會被實例化
    private Singleton (){}  
    //提供統一的外部訪問方法,獲取唯一可用的對象
    public static Singleton getInstance() {  
    	return instance;  
    }  
}

測試

public class Main {
   public static void main(String[] args) {
       
      //由於使用使用private構造函數私有化 所以使用new關鍵字創建對象會報錯。
      //Singleton instance = new Singleton();
       
      //多次獲取的是同一個對象
      Singleton instance1 = Singleton.getInstance();
      Singleton instance2 = Singleton.getInstance();
   }
}

優化-final

可以看到instance對象創建後是不會再被更改的,所以可以使用final關鍵字修飾一下。

    private static final Singleton instance = new Singleton(); 

2.懶漢式

**是否 Lazy 初始化:**是

**是否多線程安全:**否

**實現難度:**易

**描述:**這種方式是最基本的實現方式,這種實現最大的問題就是不支持多線程。因爲沒有加鎖 synchronized,所以嚴格意義上它並不算單例模式。
這種方式 lazy loading 很明顯,不要求線程安全,在多線程不能正常工作。

public class Singleton {  
	//先定義一個空印用,等待後續賦值
    private static Singleton instance;  
    //讓構造函數爲 private,這樣該類就不會被實例化
    private Singleton (){}  
  	//獲取實例時先判斷是否爲空,是否需要創建一個對象。
    public static Singleton getInstance() {  
        if (instance == null) {  
            instance = new Singleton();  
        }  
    	return instance;  
    }  
}

優化-加鎖同步

由於getInstance方法在多線程情況瞎可能會存在線程安全問題,所以可以把getInstance方法加鎖,來保證線程安全

    public static synchronized Singleton getInstance() {  
        if (instance == null) {  
            instance = new Singleton();  
        }  
    	return instance;  
    }  

**是否多線程安全:**是

由於對getInstance()方法做了同步處理,synchronized將導致性能開銷。如果getInstance()方
法被多個線程頻繁的調用,將會導致程序執行性能的下降。反之,如果getInstance()方法不會被
多個線程頻繁的調用,那麼這個延遲初始化方案將能提供令人滿意的性能。

在早期的JVM中,synchronized(甚至是無競爭的synchronized)存在巨大的性能開銷。因此,
人們想出了一個“聰明”的技巧:雙重檢查鎖定(Double-Checked Locking)。人們想通過雙重檢查
鎖定來降低同步的開銷。下面來介紹雙重檢查鎖定來實現延遲初始化。

3.DCL雙檢鎖/雙重校驗鎖

即double-checked locking

**JDK 版本:**JDK1.5 起

**是否 Lazy 初始化:**是

**是否多線程安全:**是

**實現難度:**較複雜

**描述:**這種方式採用雙鎖機制,安全且在多線程情況下能保持高性能。
getInstance() 的性能對應用程序很關鍵。

public class Singleton {  
    private static Singleton singleton;  
    private Singleton (){}  
    public static Singleton getSingleton() {  
        if (singleton == null) {  						//第一次檢查
            synchronized (Singleton.class) {  			//加鎖
                if (singleton == null) {  				//第二次檢測
                    singleton = new Singleton();  		//【注意此處有問題】
                }  
            }  
        }  
    return singleton;  
    }  
}

重排序問題

如果第一次檢查instance不爲null,那麼就不需要執行下面的加鎖和初始化操作。因此,可以大幅降低synchronized帶來的性能開銷。多個線程試圖在同一時間創建對象時,會通過加鎖來保證只有一個線程能創建對象。在對象創建好之後,執行getInstance()方法將不需要獲取鎖,直接返回已創建好的對象。

雙重檢查鎖定看起來似乎很完美,但這是一個錯誤的優化!在線程執行到第一次檢測時,代碼讀取到instance不爲null時,instance引用的對象有可能還沒有完成初始化.

注意上述代碼種表注了一個問題。new Singleton();在JVM中可以分解爲一下三個步驟

memory = allocate();  // 1:分配對象的內存空間
ctorInstance(memory);  // 2:初始化對象
instance = memory;    // 3:設置instance指向剛分配的內存地址

上面3行僞代碼中的2和3之間,可能會被重排序(在一些JIT編譯器上,這種重排序是真實發生的)。2和3之間重排序之後的執行時序如下。

memory = allocate();  // 1:分配對象的內存空間
instance = memory;    // 3:設置instance指向剛分配的內存地址
// 注意,此時對象還沒有被初始化!
ctorInstance(memory);  // 2:初始化對象

也就是說,在多線程環境下,可能會有一下的執行順序

多線程執行時序表

線程A 線程B
A1:分配對象的內存空間
A3:設置instance指向內存空間
B1:第一次檢測非空狀態
B2:由於instance不爲空,線程B開始印用instance的對象
A2:初始化對象
A4:訪問instance對象

上面錯誤雙重檢查鎖定的示例代碼中,如果線程A 獲取到鎖進入創建對象實例,這個時候發生了指令重排序。當線程A執行到 A3 時刻,線程 B 剛好進入,由於此時對象已經不爲 Null,所以線程 B 可以自由訪問該對象。然後該對象還未初始化,所以線程 B 訪問時將會發生異常。

此問題的主要原因就是發生了指令重排序;

volatile 作用

正確的雙重檢查鎖定模式需要使用 volatilevolatile主要包含兩個功能。

  1. 保證可見性。使用 volatile 定義的變量,將會保證對所有線程的可見性。
  2. 禁止指令重排序優化。

由於 volatile 禁止對象創建時指令之間重排序,所以其他線程不會訪問到一個未初始化的對象,從而保證安全性。

注意,volatile禁止指令重排序在 JDK 5 之後才被修復

也就是將Singleton聲明爲volatile 類型即可解決問題

優化-基於volatile 的雙重檢查鎖

public class Singleton {  
    private static volatile Singleton singleton;  	//加上volatile
    private Singleton (){}  
    public static Singleton getSingleton() {  
        if (singleton == null) {  						
            synchronized (Singleton.class) {  			
                if (singleton == null) {  				
                    singleton = new Singleton();  		
                }  
            }  
        }  
    return singleton;  
    }  
}

4.IODH按需初始化持有者

即Initialization On Demand Holder idiom

**是否 Lazy 初始化:**是

**是否多線程安全:**是

**實現難度:**一般

**描述:**這種方式能達到雙檢鎖方式一樣的功效,但實現更簡單。對靜態域使用延遲初始化,應使用這種方式而不是雙檢鎖方式。這種方式只適用於靜態域的情況,雙檢鎖方式可在實例域需要延遲初始化時使用。
這種方式同樣利用了 classloader 機制來保證初始化 instance 時只有一個線程,它跟餓漢式不同的是:餓漢式只要 Singleton 類被裝載了,那麼 instance 就會被實例化(沒有達到 lazy loading 效果),而這種方式是 Singleton 類被裝載了,instance 不一定被初始化。因爲 SingletonHolder 類沒有被主動使用,只有通過顯式調用 getInstance 方法時,纔會顯式裝載 SingletonHolder 類,從而實例化 instance。想象一下,如果實例化 instance 很消耗資源,所以想讓它延遲加載,另外一方面,又不希望在 Singleton 類加載時就實例化,因爲不能確保 Singleton 類還可能在其他的地方被主動使用從而被加載,那麼這個時候實例化 instance 顯然是不合適的。這個時候,這種方式相比餓漢式就顯得很合理。

public class Singleton {  
	//定義一個私有內部類
    private static class SingletonHolder {  
    	private static final Singleton INSTANCE = new Singleton();  
    } 
    //私有構造方法
    private Singleton (){}
    //這裏將導致InstanceHolder類被初始化
    public static final Singleton getInstance() {  
    	return SingletonHolder.INSTANCE;  
    }  
    
}

兩個線程併發執行getInstance()方法,下面是執行的示意圖,圖來自於《Java併發編程的藝術》

在這裏插入圖片描述

這個方案允許new Singleton(); 創建時的重排序,但不允許非構造線程(這裏指線程B)“看到”這個重排序。

反射問題

好了現在已經有了很多種的實現方式了。也解決了線程安全和按需加載的問題。下面說一個讓人絕望的問題。

如果客戶端使用反射機制,藉助AccessibleObject.setAccessible(true)方法,那麼就可以用反射的方式調用private修飾的私有方法。😢 辛辛苦苦大半年,一個反射回到解放前。

public static void main(String[] args) throws Exception  {
    Class<?> classType = Singleton.class;  
    Constructor<?> c = classType.getDeclaredConstructor(null); 
	c.setAccessible(true);  
	Singleton s1 = (Singleton)c.newInstance();
	Singleton s2 = Singleton.getInstance();
	System.out.println(s1==s2);
}  

繼續優化 😭

參考《effective java3》第3條 用私有構造器或者枚舉類型強化 Singleton屬性

私有構造函數異常處理

public class Singleton {  
	pprivate static volatile boolean flag = false; 
    //私有構造方法
    private Singleton (){
    	synchronized(Singleton.class){
    	    if(flag == false)  {  
                flag = !flag;  
            }else {
                throw new RuntimeException("狗東西你想幹嗎?");  
            }  
    	}
    }
    
    //定義一個私有內部類
    private static class SingletonHolder {  
    	private static final Singleton INSTANCE = new Singleton();  
    } 
    //這裏將導致InstanceHolder類被初始化
    public static final Singleton getInstance() {  
    	return SingletonHolder.INSTANCE;  
    }  
    
}

5.枚舉實現單例

**JDK 版本:**JDK1.5 起

**是否 Lazy 初始化:**否

**是否多線程安全:**是

**實現難度:**易

**描述:**這種實現方式還沒有被廣泛採用,但這是實現單例模式的最佳方法。它更簡潔,自動支持序列化機制,絕對防止多次實例化。
這種方式是 Effective Java 作者 Josh Bloch 提倡的方式,它不僅能避免多線程同步問題,而且還自動支持序列化機制,防止反序列化重新創建新的對象,絕對防止多次實例化。不過,由於 JDK1.5 之後才加入 enum 特性,用這種方式寫不免讓人感覺生疏,在實際工作中,也很少用。
不能通過 reflection attack 來調用私有構造方法。

public enum Singleton{
	INSTANCE;
}

使用推薦

一般情況下,不建議使用懶漢式,建議使用線程安全的餓漢式。只有在要明確實現 lazy loading 效果時,纔會使用IODH的方式。如果涉及到反序列化創建對象時,可以嘗試使用枚舉方式。如果有其他特殊的需求,可以考慮使用雙檢鎖方式。

參考

《java併發編程的藝術》

《effective java3》

菜鳥教程-單例模式

爲什麼雙重檢查鎖模式需要 volatile ?

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章