JAVA如何正確寫出一個單例模式?看這裏就夠了

什麼是單例模式?

保證一個類只有一個實例,且在類裏面提供一個全局可以訪問的入口。如圖 Singleton 類,提供了一個 getInstance() 入口獲取這個實例。
類圖

爲什麼需要單例?

  • 節省內存
  • 節省計算
  • 保證結果的正確(需要一個全局的計數器)
  • 方便管理(很多工具類只需要一個實例)

很多類並不需要創建大量的實例。如:初始化時的類,在第一次構造的時候花了大量的時間進行初始化該對象。

public class ExpensiveResource {
    public ExpensiveResource () {
        field1 = // 查詢數據庫
        field2 = // 對查詢的數據進行計算
        field3 = // 加密、壓縮等耗時操作
    }
}

適用場景

適用場景

常見的單例模式寫法

  • 餓漢式
  • 懶漢式
  • 雙重檢查式(又名double-check)
  • 靜態內部類式
  • 枚舉式(目前最好的實現方式)

下面就是從簡單到最後的枚舉式,逐步遞增,去看看這個單例是怎麼寫的。各個方式有什麼弊端,才最後產生了枚舉式。

餓漢式

寫法1public class Singleton {
    private static Singleton singlenton = new Singlention();
    
    private Singleton() {}
    
    public Singlenton getInstance() {
        return singlenton;
    }
}

---------------------------------------------------------
寫法2public class Singleton() {
    private static Singleton singleton;
    
    static {
        singleton = new Singleton();
    }
    
    private Singleton() {}
    
    public Singlenton getInstance() {
        return singlenton;
    }
}

如上所示:

  1. static 來修飾實例
  2. 構造函數用 private 修飾
  3. 實例直接在加載時候 new.

寫法最簡單,在類裝載的時候就實例化了,避免線程同步的問題。
但是 缺點是類加載就完成了實例化 。沒有達到懶加載。

很明顯,如果永遠都用不上這個類的話,那就資源浪費了。所以產生了下面的懶漢式。

懶漢式

線程不安全的寫法:

public class Singleton {
    private static Singleton singlenton;
    
    private Singleton() {}
    
    public Singlenton getInstance() {
        if (singleton == null) {
            singleton = new Singleton();
        }
        return singleton;
    }
}

很明顯在 getInstance() 中增加了判斷,在獲取的時候纔去實例化。這個就是懶加載了。

但是注意了,這種只適合單線程,如果多線程,就會出現實例化多次的情況。場景重現:

線程1:進入了 if 語句,準備進行實例化。此時線程2進來了,線程1被掛起。

線程2:進入 if 語句,創建完實例後,切換回線程1 繼續執行。

線程1:繼續實例化 Singleton

這個時候就出現了初始化多次了。很明顯多線程下->這是一個錯誤的寫法

線程安全的寫法(改寫 getInstance()方法):

public class Singleton {
    private static Singleton singlenton;
    
    private Singleton() {}
    
    public static synchronized Singlenton getInstance() {
        if (singleton == null) {
            singleton = new Singleton();
        }
        return singleton;
    }
}

增加鎖,鎖住整個方法。
但是缺點是:執行效率十分低。因爲每個線程 getInstance() 都要進行同步,多個線程的時候,必須要等待一個一個的來。

所以我們再升級一下寫法,再改改 getInstance():

public static synchronized Singlenton getInstance() {
    if (singleton == null) {
        synchronzied (Singleton.class) {
            singleton = new Singleton();    
        }
    }
    return singleton;
}

不過還是有問題的,一樣的併發情況下,還是會出現上面線程不安全的問題。

左思右想,還是不行。所以最後爲了解決這個問題。就出現了 double-check雙重檢查式。

雙重檢查式(double-check)

public class Singleton {
    private static volatile Singleton singlenton;
    
    private Singleton() {}
    
    public Singlenton getInstance() {
        if (singleton == null) {
            synchronzied (Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

有幾個地方發生了改變:

  • getInstance() 時進行了兩次判斷。
  • Singleton 增加了 volatile 可見性關鍵字,volatile和synchronized不同見我之前的博文:https://blog.csdn.net/Charlven/article/details/104463842。

通過 volatile 和 兩次判空結合。就可以實現線程安全了。
優點:

  1. 線程安全
  2. 延遲加載
  3. 效率更高

可能有的小夥伴會不明白,爲什麼一定要兩次 check,那我們來試試去掉其中一個check。

  1. 去掉第一個 check:很明顯的,去掉第一個check,就會全部都進入同步了。效率很慢,原因上面已經說了。
  2. 去掉第二個 check:會出現併發問題。當singleton=null,同時兩個線程都執行到第一個 check 時候,線程1進入sync代碼塊,線程2在外面等待;線程1執行完,線程2繼續創建實例。就會同時創建了兩個實例了。

爲什麼要 votatile 呢?

因爲實例化 singleton 並不是一個原子性操作。JVM 可能存在着重排序。我們來看看實例化的流程:
實例化順序
存在重排序完了之後變成:
重排序後

所以當多線程的時候,且執行順序被重排序時,程序就會報錯。詳細我們來看看:

在這裏插入圖片描述

所以使用volatile的意義主要在於防止重排序的情況。避免拿到未完成初始化的對象。

靜態內部類

public class Singleton {
    private Singleton() {}
    
    private static class SingletonInstance() {
        private static final Singleton singleton = new singleton(); 
    }
    
    public Singlenton getInstance() {
        return SingletonInstance.singleton;
    }
}

和餓漢式的方式類似。不過這個是通過 JVM 的方式保證了線程安全。
和餓漢式不一樣的是,這個是懶加載模式的。也就是只有在 getInstance() 纔去實例化。
所以靜態內部類的寫法跟雙重檢查式的優點是一致的。

  1. 線程安全
  2. 延遲加載,效率高。

看着似乎完美了,無可挑剔。但是靜態內部類和雙重檢查式都存在着一個缺點:

可以被反序列化。通過反射可以創建多個實例。

所以這個時候就來一個終極推薦寫法 ↓

枚舉式

public enum Singleton {
    INSTANCE;
    public void whateverMethod() {
        
    }
}

優點:

  1. 簡潔
  2. 線程安全有保障
    1. 反編譯可以看出都是 static代碼塊
    2. 在類被加載時完成初始化,加載由JVM保證線程安全
  3. 防止被反序列化破壞,反序列時只能根據valueOf方法來查看,而不能新建對象。
  4. 反射時是不能創建對象。

總結

單例主要就是

  • 餓漢式
  • 懶漢式
  • 雙重檢查式(又名double-check)
  • 靜態內部類式
  • 枚舉式(目前最好的實現方式)

上面幾種,單線程時推薦使用懶漢式。而大多數建議後 3 種實現方式。

枚舉式目前可能比較少人寫,但是也是極爲推薦的。最後該文章是參考了極客時間某篇課程輸出的。

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