設計模式:單例模式 (關於餓漢式和懶漢式)

xue無止境發表於2019-05-24

定義

單例模式是比較常見的一種設計模式,目的是保證一個類只能有一個例項,而且自行例項化並向整個系統提供這個例項,避免頻繁建立物件,節約記憶體。

單例模式的應用場景很多,

比如我們電腦的作業系統的回收站就是一個很好的單例模式應用,電腦上的檔案、視訊、音樂等被刪除後都會進入到回收站中;還有計算機中的印表機也是採用單例模式設計的,一個系統中可以存在多個列印任務,但是隻能有一個正在工作的任務;Web頁面的計數器也是用單例模式實現的,可以不用把每次重新整理都記錄到資料庫中。

通過回味這些應用場景,我們對單例模式的核心思想也就有了更清晰的認識,下面就開始用程式碼來實現。

在寫單例模式的程式碼之前,我們先簡單瞭解一下兩個知識點,關於類的載入順序和static關鍵字。

類載入順序

類載入(classLoader)機制一般遵從下面的載入順序

如果類還沒有被載入:

  • 先執行父類的靜態程式碼塊和靜態變數初始化,靜態程式碼塊和靜態變數的執行順序跟程式碼中出現的順序有關。
  • 執行子類的靜態程式碼塊和靜態變數初始化。
  • 執行父類的例項變數初始化
  • 執行父類的建構函式
  • 執行子類的例項變數初始化
  • 執行子類的建構函式

同時,載入類的過程是執行緒私有的,別的執行緒無法進入。

如果類已經被載入:

靜態程式碼塊和靜態變數不在重複執行,再建立類物件時,只執行與例項相關的變數初始化和構造方法。

static關鍵字

一個類中如果有成員變數或者方法被static關鍵字修飾,那麼該成員變數或方法將獨立於該類的任何物件。它不依賴類特定的例項,被類的所有例項共享,只要這個類被載入,該成員變數或方法就可以通過類名去進行訪問,它的作用用一句話來描述就是,不用建立物件就可以呼叫方法或者變數,這簡直就是為單例模式的程式碼實現量身打造的。

下面將列舉幾種單例模式的實現方式,其關鍵方法都是用static修飾的,並且,為了避免單例的類被頻繁建立物件,我們可以用private的建構函式來確保單例類無法被外部例項化。

懶漢和餓漢

在程式編寫上,一般將單例模式分為兩種,分別是餓漢式和懶漢式,

餓漢式:在類載入時就完成了初始化,所以類載入比較慢,但獲取物件的速度快。

懶漢式:在類載入時不初始化,等到第一次被使用時才初始化。

程式碼實現

1、餓漢式 (可用)

public class Singleton {

    private final static Singleton INSTANCE = new Singleton();
    
    private Singleton(){}

    public static Singleton getInstance(){
        return INSTANCE;
    }

}

這是比較常見的寫法,在類載入的時候就完成了例項化,避免了多執行緒的同步問題。當然缺點也是有的,因為類載入時就例項化了,沒有達到Lazy Loading (懶載入) 的效果,如果該例項沒被使用,記憶體就浪費了。

2、普通的懶漢式 (執行緒不安全,不可用)

public class Singleton {

    private static Singleton instance = null;

    private Singleton() {
    }

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

}

這是懶漢式中最簡單的一種寫法,只有在方法第一次被訪問時才會例項化,達到了懶載入的效果。但是這種寫法有個致命的問題,就是多執行緒的安全問題。假設物件還沒被例項化,然後有兩個執行緒同時訪問,那麼就可能出現多次例項化的結果,所以這種寫法不可採用。

3、同步方法的懶漢式 (可用)

public class Singleton {

    private static Singleton instance = null;

    private Singleton() {
    }

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

}

這種寫法是對getInstance()加了鎖的處理,保證了同一時刻只能有一個執行緒訪問並獲得例項,但是缺點也很明顯,因為synchronized是修飾整個方法,每個執行緒訪問都要進行同步,而其實這個方法只執行一次例項化程式碼就夠了,每次都同步方法顯然效率低下,為了改進這種寫法,就有了下面的雙重檢查懶漢式。

4、雙重檢查懶漢式 (可用,推薦)

public class Singleton {

    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;
    }

}

這種寫法用了兩個if判斷,也就是Double-Check,並且同步的不是方法,而是程式碼塊,效率較高,是對第三種寫法的改進。為什麼要做兩次判斷呢?這是為了執行緒安全考慮,還是那個場景,物件還沒例項化,兩個執行緒A和B同時訪問靜態方法並同時執行到第一個if判斷語句,這時執行緒A先進入同步程式碼塊中例項化物件,結束之後執行緒B也進入同步程式碼塊,如果沒有第二個if判斷語句,那麼執行緒B也同樣會執行例項化物件的操作了。

5、靜態內部類 (可用,推薦)

public class Singleton {

    private Singleton() {}

    private static class SingletonInstance {
        private static final Singleton INSTANCE = new Singleton();
    }

    public static Singleton getInstance() {
        return SingletonInstance.INSTANCE;
    }

}

這是很多開發者推薦的一種寫法,這種靜態內部類方式在Singleton類被裝載時並不會立即例項化,而是在需要例項化時,呼叫getInstance方法,才會裝載SingletonInstance類,從而完成物件的例項化。

同時,因為類的靜態屬性只會在第一次載入類的時候初始化,也就保證了SingletonInstance中的物件只會被例項化一次,並且這個過程也是執行緒安全的。

6、列舉 (可用、推薦)

public enum Singleton {
    INSTANCE;
}

這種寫法在《Effective JAVA》中大為推崇,它可以解決兩個問題:

1)執行緒安全問題。因為Java虛擬機器在載入列舉類的時候會使用ClassLoader的方法,這個方法使用了同步程式碼塊來保證執行緒安全。

2)避免反序列化破壞物件,因為列舉的反序列化並不通過反射實現。

好了,單例模式的幾種寫法就介紹到這了,最後簡單總結一下單例模式的優缺點

單例模式的優缺點

優點

單例類只有一個例項,節省了記憶體資源,對於一些需要頻繁建立銷燬的物件,使用單例模式可以提高系統效能;

單例模式可以在系統設定全域性的訪問點,優化和共享資料,例如前面說的Web應用的頁面計數器就可以用單例模式實現計數值的儲存。

缺點

單例模式一般沒有介面,擴充套件的話除了修改程式碼基本上沒有其他途徑。

相關文章