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


定義

單例模式是比較常見的一種設計模式,目的是保證一個類只能有一個實例,而且自行實例化並向整個系統提供這個實例,避免頻繁創建對象,節約內存。

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

比如我們電腦的操作系統的回收站就是一個很好的單例模式應用,電腦上的文件、視頻、音樂等被刪除后都會進入到回收站中;還有計算機中的打印機也是采用單例模式設計的,一個系統中可以存在多個打印任務,但是只能有一個正在工作的任務;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應用的頁面計數器就可以用單例模式實現計數值的保存。

缺點

單例模式一般沒有接口,擴展的話除了修改代碼基本上沒有其他途徑。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM