文章目錄
什麼是單例模式?
保證一個類只有一個實例,且在類裏面提供一個全局可以訪問的入口。如圖 Singleton 類,提供了一個 getInstance() 入口獲取這個實例。
爲什麼需要單例?
- 節省內存
- 節省計算
- 保證結果的正確(需要一個全局的計數器)
- 方便管理(很多工具類只需要一個實例)
很多類並不需要創建大量的實例。如:初始化時的類,在第一次構造的時候花了大量的時間進行初始化該對象。
public class ExpensiveResource {
public ExpensiveResource () {
field1 = // 查詢數據庫
field2 = // 對查詢的數據進行計算
field3 = // 加密、壓縮等耗時操作
}
}
適用場景
常見的單例模式寫法
- 餓漢式
- 懶漢式
- 雙重檢查式(又名double-check)
- 靜態內部類式
- 枚舉式(目前最好的實現方式)
下面就是從簡單到最後的枚舉式,逐步遞增,去看看這個單例是怎麼寫的。各個方式有什麼弊端,才最後產生了枚舉式。
餓漢式
寫法1:
public class Singleton {
private static Singleton singlenton = new Singlention();
private Singleton() {}
public Singlenton getInstance() {
return singlenton;
}
}
---------------------------------------------------------
寫法2:
public class Singleton() {
private static Singleton singleton;
static {
singleton = new Singleton();
}
private Singleton() {}
public Singlenton getInstance() {
return singlenton;
}
}
如上所示:
- static 來修飾實例
- 構造函數用 private 修飾
- 實例直接在加載時候 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 和 兩次判空結合。就可以實現線程安全了。
優點:
- 線程安全
- 延遲加載
- 效率更高
可能有的小夥伴會不明白,爲什麼一定要兩次 check,那我們來試試去掉其中一個check。
- 去掉第一個 check:很明顯的,去掉第一個check,就會全部都進入同步了。效率很慢,原因上面已經說了。
- 去掉第二個 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()
纔去實例化。
所以靜態內部類的寫法跟雙重檢查式的優點是一致的。
- 線程安全
- 延遲加載,效率高。
看着似乎完美了,無可挑剔。但是靜態內部類和雙重檢查式都存在着一個缺點:
可以被反序列化。通過反射可以創建多個實例。
所以這個時候就來一個終極推薦寫法 ↓
枚舉式
public enum Singleton {
INSTANCE;
public void whateverMethod() {
}
}
優點:
- 簡潔
- 線程安全有保障
- 反編譯可以看出都是 static代碼塊
- 在類被加載時完成初始化,加載由JVM保證線程安全
- 防止被反序列化破壞,反序列時只能根據valueOf方法來查看,而不能新建對象。
- 反射時是不能創建對象。
總結
單例主要就是
- 餓漢式
- 懶漢式
- 雙重檢查式(又名double-check)
- 靜態內部類式
- 枚舉式(目前最好的實現方式)
上面幾種,單線程時推薦使用懶漢式。而大多數建議後 3 種實現方式。
枚舉式目前可能比較少人寫,但是也是極爲推薦的。最後該文章是參考了極客時間某篇課程輸出的。