單例模式的各種實現方式(Java)

酒冽 發表於 2022-01-26
Java

單例模式的基礎實現方式

手寫普通的單例模式要點有三個:

  • 將建構函式私有化
  • 利用靜態變數來儲存全域性唯一的單例物件
  • 使用靜態方法 getInstance() 獲取單例物件

懶漢模式

懶漢模式指的是單例物件的延遲載入,只有在呼叫 getInstance() 獲取單例物件時才會將單例建立出來。懶漢模式適用於對記憶體要求高的場景。程式碼如下:

public class Singleton {
    private static Singleton instance;

    private Singleton() {}

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

餓漢模式

與懶漢模式相對的是餓漢模式,適用於對記憶體要求不高的場景,在類載入的初始化階段就完成了單例物件的建立,程式碼如下:

public class Singleton {
    // 靜態變數初始化
    private static Singleton instance = new Singleton();

    private Singleton() {}

    public static Singleton getInstance() {
        return instance;
    }
}

靜態變數的初始化是在類載入階段的初始化過程進行,在此期間,編譯器會自動收集類中所有靜態變數的賦值動作和 static 塊,生成 <clinit> 方法並執行。比較特殊的一點是,如果多個執行緒同時初始化 Singleton 類,JVM 會保證只有一個執行緒能夠執行 Singleton 類的 <clinit> 方法,其他執行緒都必須阻塞等待。而且同一個類載入器下,一個類只會被初始化一次,即 <clinit> 方法只會被執行一次,這就保證了多執行緒下單例物件只會被建立一次

作者:酒冽        出處:https://www.cnblogs.com/frankiedyz/p/15847802.html
版權:本文版權歸作者和部落格園共有
轉載:歡迎轉載,但未經作者同意,必須保留此段宣告;必須在文章中給出原文連線;否則必究法律責任

多執行緒下的單例模式

單例模式需要保證的一點是,在整個程式執行期間,單例物件只會被建立一次。如果是單執行緒環境中,這一點很好保證。但如果是多執行緒環境中,保證這一點並不簡單

上面已經說過,餓漢模式的單例模式下,JVM 會保證單例物件只會被建立一次,因此可以保證這一點。而懶漢模式在多執行緒環境中不能保證這一點,接下來討論的是對懶漢模式進行改造,讓它能夠保證這一點

使用synchronized方法

最簡單直接的方式就是為 getInstance() 加上 synchronized 關鍵字,這樣確實可以保證多執行緒環境中,單例物件只會被建立一次。但是 synchronized 方法最大的缺點在於它將獲取單例物件這一行為徹底序列化,同一時刻只能有一個執行緒能執行 getInstance() ,大大降低了併發效率
程式碼如下:

public class Singleton {
    private static Singleton instance;

    private Singleton() {}

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

雙重檢測鎖

直接使用 synchronized 方法降低效率的主要原因在於,synchronized 方法的加鎖粒度太粗,那麼將鎖的範圍縮小,就可以緩解這一問題,而雙重檢測鎖就是這麼實現的。不過為了保證併發的正確性,在內部又加了一道檢測,故名為雙重檢測鎖。程式碼如下:

public class Singleton {
    // 這裡的instance一定要定義為volatile變數!!!
    private static volatile Singleton instance;

    private Singleton() {}

    public static Singleton getInstance() {
        // 雙重鎖檢測
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

上面程式碼的關鍵點有三個:

  • synchronized 加鎖的範圍更小,這是為了更高的併發效率
  • synchronized 內部還有一道檢測,如果執行緒1進入了同步塊,但還未將單例物件建立出來,此時執行緒2正好繞過了第一道檢測,在同步塊外等待獲取鎖定。因此同步塊內也要加上一道檢測,避免單例物件被重複建立
  • instance 這個變數一定要宣告為 volatilevolatile 在這裡最大的作用是禁止指令重排序。如果不加 volatile 修飾,由於 instance = new Singleton() 可能被重排序而導致在這條語句執行過程中,instance 率先被分配記憶體並獲得地址,成為非 null,但建構函式卻沒有真正執行完畢,此時別的執行緒可能拿到的 instance 就是不完全構造的單例物件

instance = new Singleton() 這條語句正常的執行順序是:
1、為即將建立的物件分配一塊記憶體
2、執行建構函式中的語句,對記憶體進行相應的讀寫操作
3、讓 instance 指向這塊記憶體
在重排序情況下順序可能是 1 -> 3 -> 2,當執行到3時 instance 就成為非 null,此時其他執行緒如果引用了 instance,拿到的就是一個不完全構造的物件

需要注意的是,在 JDK5 之前,就算加了 volatile 關鍵字也依然有問題,原因是之前的 JMM 是有缺陷,volatile 變數前後的程式碼仍然可以出現重排序問題,這個問題在 JDK5 之後才得到解決,所以現在才可以這麼使用

作者:酒冽        出處:https://www.cnblogs.com/frankiedyz/p/15847802.html
版權:本文版權歸作者和部落格園共有
轉載:歡迎轉載,但未經作者同意,必須保留此段宣告;必須在文章中給出原文連線;否則必究法律責任

其他單例模式的實現方式

基於列舉類

基於列舉類的方式非常簡潔,只要簡單地編寫一個只包含一個元素的列舉類,由 JVM 來保證單例的唯一性和執行緒安全性,自帶私有的構造方法並且序列化和反射都不會破壞單例的唯一性,據說是 JDK5 之後最好的單例建立方式

public enum Singleton {
    instance;
    
    // 定義各種欄位、方法
    
    private Singleton() {}
    
    public static Singleton getInstance() {
        return instance;
    }
}

其中列舉類的構造器不用特意加上 private 修飾,因為列舉類構造器預設就是 private 的,且只能使用 private 修飾

簡單理解列舉實現單例的過程:程式啟動時,會自動呼叫 Singleton 的構造器,例項化單例物件並賦給 instance,之後再也不會例項化,這也是一個餓漢過程,即使沒有呼叫過 getInstance(),也會將單例物件建立出來

使用列舉來建立單例模式的優勢有3點:

  • 程式碼量更少,更加簡潔
  • 沒有做任何額外的操作,就可以保證單例的唯一性和執行緒安全性
  • 使用列舉類可以防止呼叫者使用反射、序列化和反序列化機制強制生成多個單例物件,破壞唯一性

這第三點優勢讓基於列舉類的單例模式變得“無懈可擊”了,列舉類可以保證唯一性的原理如下:

  • 防反射
單例模式的各種實現方式(Java)

列舉類預設繼承了 Enum 類,在利用反射呼叫 newInstance() 時,會判斷該類是否是列舉類,如果是則丟擲異常

  • 防反序列化建立多個列舉物件

對於列舉型別,由於列舉類和列舉變數的組合名是唯一的,可以唯一確定物件。因此,序列化只會將列舉類名 + 列舉變數名輸出到檔案中。反序列化時,讀入的就是列舉類名 + 列舉變數名,再根據 Enum 類的 valueOf 方法,在記憶體中找對已經存在的列舉物件,並不會建立新的物件

類載入器對單例模式的影響

同一個類載入器對一個類只會載入一次,但是不同的類載入器可能會多次載入同一個類,如果程式中有多個類載入器,需要在單例中指定某個特定的類載入器,並保證這個類載入器始終是同一個