Java設計模式——單例模式(建立型模式)

Zhaoxi_Zhang發表於2018-12-15

概述

  單例模式保證對於每一個類載入器,一個類僅有一個例項並且提供全域性的訪問。其是一種物件建立型模式。對於單例模式主要適用以下幾個場景:

  • 系統只需要一個例項物件,如提供一個唯一的序列號生成器
  • 客戶呼叫類的單個例項只允許使用一個公共訪問點,除了該公共訪問點,不能通過其他途徑訪問該例項

  單例模式的缺點之一是在分散式環境中,如果因為單例模式而產生 bugs,那麼很難通過除錯找出問題所在,因為在單個類載入器下進行除錯,並不會出現問題。

實現方式

  一般來說,實現列舉有五種方式:餓漢式、懶漢式、雙重鎖檢驗、靜態內部類、列舉,而這裡我將這五種方式分為三部分來介紹。

餓漢式載入

public final class Singleton {
    //私有構造器,所以無法例項化類物件
    private Singleton() {}

    //類靜態例項域
    private static final Singleton INSTANCE = new Singleton();

    //返回類例項
    public static Singleton getInstance() {
        return INSTANCE;
    }
}
複製程式碼

直接初始化靜態例項保證了執行緒安全,但是此種方式不是懶載入的,單例一開始就初始化了,無法在我們需要的時候再進行初始化。

懶漢式載入

//例項在這個方法第一次被呼叫的時候進行初始化
public static synchronized Singleton getInstance() {
    if (instance == null) {
        instance = new Singleton();
    }
    return instance;
}
複製程式碼

getInstance()方法設定為synchronized保證了執行緒安全,但是其效率並不高,因為在任何時候只有一個執行緒能夠訪問這個方法,而同步操作僅需在第一次被呼叫的時候才被需要。

此方法的一種改進是使用雙重檢驗鎖。

public final class ThreadSafeDoubleCheckLocking {

    private static volatile ThreadSafeDoubleCheckLocking instance;

    private ThreadSafeDoubleCheckLocking() {}

    public static ThreadSafeDoubleCheckLocking getInstance() {
        //區域性變數可以提高25%的效能,這個區域性變數確保instance只在已經被初始化的情況下讀取一次
        //《Effective Java 第2版》P250頁
        ThreadSafeDoubleCheckLocking result = instance;
        //檢查例項是否已經別初始化
        if (result == null) {
            //未被初始化,但是無法確定這時其他執行緒是否已經對其初始化,因此新增物件鎖進行互斥
            synchronized (ThreadSafeDoubleCheckLocking.class) {
                //再一次將instance賦值給區域性變數來進行檢查,因為有可能在當前執行緒阻塞的時候,其他執行緒對instance進行初始化
                result = instance;
                if (result == null) {
                    //此時還未被初始化的話,在這裡初始化可以保證執行緒安全
                    instance = result = new ThreadSafeDoubleCheckLocking();
                }
            }
        }
        return result;
    }
}
複製程式碼

上面的雙重檢驗鎖使用了《Effective Java 第2版》提出的一個優化方式,另外值得一提的是,對於instance域被宣告為volatile是很重要的。當一個變數定義為volatile之後,它就具備了兩種特性,第一是保證了此變數對所有執行緒的可見性,“可見性”指的是當一條執行緒修改了這個變數的值,新值對於其他執行緒來說是可以立即得知的(注意基於volatile變數的運算在併發程式設計下並非是安全的,例如:假設被volatile修飾的域進行自增運算,而自增運算並不是原子操作,那麼第二個執行緒就可能在讀取舊值和寫回新值的期間讀取到這個域,導致第二個執行緒看到的值與第一個執行緒未自增前的值一樣,詳細瞭解的話可檢視《深入理解Java虛擬機器 第2版》P366 基於volatile型變數的特殊規則);第二是禁止指令重排序優化。 在進行初始化的時候instance = result = new ThreadSafeDoubleCheckLocking(),此時 JVM 大致做了三件事:

  • 1.給instance分配記憶體
  • 2.呼叫建構函式進行初始化
  • 3.instance物件指向被分配的記憶體

沒有宣告為volatile,那麼指令重排序後,可能執行的順序是 1-3-2,當執行緒一執行到3這個步驟,還未執行步驟2(instance非null,但未初始化),那麼對於執行緒二,此時檢測到 instance 並非是 null,直接返回 instance,就會出現錯誤。需要說明的一點是,JDK 1.5以後,volatile才真正發揮用處,因此在1.5以前,仍然是無法保證安全的,具體可檢視 The "Double-Checked Locking is Broken" Declaration.

另外一種懶載入方式就是使用靜態內部類的方法:

public final class InitializingOnDemandHolderIdiom {
    private InitializingOnDemandHolderIdiom() {}

    public static InitializingOnDemandHolderIdiom getInstance() {
        return HelperHolder.INSTANCE;
    }

    private static class HelperHolder {
        private static final InitializingOnDemandHolderIdiom INSTANCE =
                new InitializingOnDemandHolderIdiom();
    }
}
複製程式碼

這種方式是執行緒安全的,同時也是懶載入的。HelperHolder是私有的,除了getInstance()外沒有辦法訪問。這種方式不需要依賴其他語言特性(volatile,synchronized),也不依賴JDK版本。

列舉

《Effective Java 第2版》P15 中提到實現單例的一種新方式,使用列舉來實現單例。列舉型別是Java 5中新增特性的一部分,因此使用這種方式實現的列舉,要求至少是 JDK 1.5版本及其以上。列舉本身保證了執行緒安全,並且提供了序列化機制,因此這種方式寫起來極為簡潔。

public enum Singleton {
    INSTANCE;
}
複製程式碼

當然,對於使用列舉來實現單例模式也有一些缺點,具體可以檢視 StackExchange 的討論。

典型使用場景

  • 日誌紀錄類
  • 管理與資料庫的連線
  • 檔案管理系統

具體例項

java.lang.Runtime#getRuntime() java.awt.Desktop#getDesktop() java.lang.System#getSecurityManager()

參考資料

相關文章