設計模式之單例模式

2022-07-18 06:02:59

1、什麼是單例模式

​ 單例模式是指保證某個類在整個軟體系統中只有一個物件範例,並且該類僅提供一個返回其物件範例的方法(通常為靜態方法)

2、單例模式的種類

​ 經典的單例模式實現方式一般有五種:

2.1 餓漢式

// ①餓漢式:使用靜態常數
static class Singleton {
    // 1.構造器私有化,其他類不能new
    private Singleton() {}
    // 2.類的內部建立物件
    private final static Singleton instance = new Singleton();
    // 3.向外部暴露一個靜態的公共方法
    public static Singleton getInstance() {
        return instance;
    }
}
// ②餓漢式:使用靜態程式碼塊
static class Singleton {
    // 1.構造器私有化,其他類不能new
    private Singleton() {}
    private static final Singleton instance;
    // 2.靜態程式碼塊範例化
    static {
        instance = new Singleton();
    }
    // 3.向外部暴露一個靜態的公共方法
    public static Singleton getInstance() {
        return instance;
    }
}

餓漢式顧名思義就是迫不及待地載入該類的物件範例,物件範例的載入最早是在類的載入過程中的初始化階段(即靜態參照變數的載入,對應位元組碼檔案中<clinit>方法的執行),載入過程由JVM保證執行緒安全。餓漢式會浪費記憶體,但是隨著計算機的發展,記憶體已經不是問題了,所以使用餓漢式也未嘗不可。

​ JDK原始碼舉例:

​ 該類位於java.lang包下,首先將構造方法私有化,宣告了一個私有的靜態變數並且對該變數進行物件範例的建立,再建立一個公有的靜態方法返回這個物件範例,這是比較常用的一種實現單例模式的方式。

2.2 懶漢式

// ①懶漢式:執行緒不安全
static class Singleton {
    // 1.構造器私有化,其他類不能new
    private Singleton() {}
    private static Singleton instance;
    // 2.向外部暴露一個靜態的公共方法
    public static Singleton getInstance() {
        // 3.instance == null時進行範例化
        if ( instance == null ) {
            // new Singleton()不是一個原子操作,JVM中會進行大致[建立物件-分配記憶體-物件初始化]等過程,在這之前instance都為null
            // 多執行緒情況下,多個執行緒同時執行到該位置,執行緒獲取到時間片後會繼續執行,就可能建立多個範例
            instance = new Singleton();
        }
        return instance;
    }
}
// ②懶漢式:執行緒安全(方法上新增 synchronized 關鍵字)
static class Singleton {
    // 1.構造器私有化,其他類不能new
    private Singleton() {}
    private static Singleton instance;
    // 2.向外部暴露一個靜態的公共方法, synchronized 保證執行緒安全
    public static synchronized Singleton getInstance() {
        // 3.instance == null時進行範例化
        if ( instance == null ) {
            instance = new Singleton();
        }
        return instance;
    }
}

​ 懶漢式就是在建立物件範例前先判斷是否已經建立,但是由於物件範例的建立並不是一個原子過程,所以會出現執行緒安全問題,可以在方法上新增synchronized解決,當然會犧牲一定的效能。基於以上原因,不推薦使用懶漢式的方式實現單例模式。

​ 如何證明物件範例的建立不是一個原子操作?位元組碼指令可以從側面證明。

// Java原始碼
public class Test {
    public Test getTest() {
        return new Test();
    }
}

紅框1的位置有三條位元組碼指令,這還只是位元組碼的層面,再往低層還會有更多的步驟,所以很明顯物件範例的建立不是一個原子操作

2.3 雙重檢查鎖

static class Singleton {
    // 1.構造器私有化,其他類不能new
    private Singleton() {}
    // 2.volatile保證多執行緒下的可見性
    private static volatile Singleton instance;
    // 3.向外部暴露一個靜態的公共方法
    public static Singleton getInstance() {
        // 3.非空判斷
        if ( instance == null ) {
            // 4.同步程式碼塊
            synchronized (Singleton.class) {
                // 5.再次非空判斷(保證多執行緒下的單例)
                if ( instance == null ) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

雙重檢查鎖是最複雜的一種單例模式實現方式,我把它拆分成三個問題:

① 為什麼synchronized不加到方法上?

​ 如果新增到方法上,兩次非空判斷就沒有必要了,一次就夠了,就轉化成了懶漢式(執行緒安全),這種方式效率不高,因為每次呼叫都需要獲取鎖和釋放鎖。

② 為什麼要做兩次非空判斷?

​ 之前也提到過:物件範例的建立不是一個原子操作。執行緒安全問題也是出在這一過程中的,解決方案就是新增synchronized關鍵字,但是新增到方法上效率又太低了;

​ 既然問題是出現在物件範例建立的過程中,那麼只對這一段程式碼進行同步操作(加鎖物件就是當前的Class物件,因為物件範例只有一個);

​ 第一層的非空判斷是為了如果物件範例已經建立完成了,就不需要再次進入同步程式碼塊了,直接返回建立好的物件範例即可。

③ 為什麼要加volatile

根據物件範例建立的位元組碼指令可以看出物件範例的建立大致分為三步:

​ ① 在堆記憶體中分配物件記憶體

​ ② 呼叫<init>方法,執行物件範例的初始化

​ ③ 將物件參照賦給靜態變數

大家應該對JMM模型happens-before有所瞭解,簡單來說JMM模型是對編譯器和處理器的約束,happens-before是對開發者的約束。

編譯器和處理器在實際執行時,為了執行效率可能會對指令進行重排序的操作,雖然單執行緒中不會影響執行結果,但是如果是多執行緒就會出現問題。

像物件範例建立過程的三條指令中②③就有可能會被優化為③②,但是①一定會先執行,因為②③依賴於①,此時執行順序為①③②,其他執行緒就會獲取到一個未初始化的物件,導致執行出錯。

volatile關鍵字的語意包含兩個:

​ ① 保證可見性

​ ② 禁止指令重排序(所以新增volatile後,執行順序就是①②③了)

​ JDK原始碼舉例:

​ 該類是位於java.lang包下的System類,經典的雙重檢查鎖實現方式。

2.4 靜態內部類

static class Singleton {
    // 1.構造器私有化,其他類不能new
    private Singleton() {}
    // 2.靜態內部類,Singleton類載入的時候不會載入內部類,只有用到內部類時才回去載入內部類(保證懶載入)
    private static class SingletonInstance {
        private static final Singleton instance = new Singleton();
    }
    // 3.向外部暴露一個靜態的公共方法,此時會裝載SingletonInstance,類裝載時是執行緒安全的(保證執行緒安全)
    public static Singleton getInstance() {
        return SingletonInstance.instance;
    }
}

​ 這是一種很巧妙的方式,相對於餓漢式來說,不需要在類的初始化階段就建立物件範例,只有在需要(即呼叫getInstance()方法)的時候才會進行物件範例建立,執行緒安全也由JVM保證。

​ JDK原始碼舉例:

​ 上圖是java.lang.Short原始碼中的內部類,將常用的整數儲存到快取池當中;下圖是存取快取池中的整數。類似的還有java.lang.Integerjava.lang.Long等包裝類。

2.5 列舉

// enum實際上是extends抽象類java.lang.Enum
enum Singleton {
    instance
}

位元組碼反編譯看下:

enum關鍵字修飾的類實際上繼承了java.lang.Enum<E extends Enum<E>

列舉類中宣告的範例實際上是public static final修飾的常數

上圖為列舉類中<clinit>方法的位元組碼指令,也就是類的初始化階段需要執行的邏輯(即將靜態變數,靜態程式碼塊整合到一塊)。

紅框1:建立Singleton列舉類物件範例,實際上呼叫了java.lang.Enum類的構造器(即<init>方法),構造器引數是("INSTANCE", 0),可以通過ldc #7iconst_0看出來;物件範例建立完成後,將範例參照賦給INSTANCE常數。

紅框2:將上一步建立的物件範例參照儲存到列舉類內部陣列$VALUES中,外部可以通過values()方法返回所有的列舉物件參照;陣列的建立是在iconst_1anewarray,意思是建立一個長度為1的參照型別陣列