Java設計模式:Singleton(單例)模式

概念定義

Singleton(單例)模式是指在程序運行期間, 某些類只實例化一次,建立一個全局惟一對象。所以,單例類只能有一個實例,且必須本身建立本身的這個惟一實例,並對外提供訪問該實例的方式。
單例模式主要是爲了不建立多個實例形成的資源浪費,以及多個實例屢次調用容易致使結果出現不一致等問題。例如,一個系統只能有一個窗口管理器或文件系統,一個程序只須要一份全局配置信息。java

應用場景

  • 資源共享的狀況下,避免因爲資源操做時致使的性能或損耗等。如緩存、日誌對象、應用配置。
  • 控制資源的狀況下,方便資源之間的互相通訊。如數據庫鏈接池、線程池等。

單例實現

根據加載的時機能夠分爲即時加載延時加載兩種模式。數據庫

即時加載

在單例類被加載時就建立單例的方式,稱爲即時加載單例(也稱餓漢式)。緩存

枚舉類單例(推薦方式)

示例代碼以下:安全

public enum EnumSingleton {
    INSTANCE;
    public static EnumSingleton getInstance() { // 照顧開發者舊有習慣
        return INSTANCE;
    }

    // 外部可調用EnumSingleton.INSTANCE.doSomething()或EnumSingleton.getInstance().doSomething()
    public void doSomething() {
        System.out.println("EnumSingleton: do something like accessing resources");
    }
}

此類單例具備如下優勢:工具

  • 簡潔高效
  • 實例是靜態的,線程安全
  • 不存在clone、反射、序列化破壞單例問題

缺點則有:性能

  • 枚舉單例不能繼承和被繼承
  • 可讀性稍低(主要由於此方式較爲"新穎")

靜態公有域單例

示例代碼以下:線程

public class StaticFieldSingleton {
    public static final StaticFieldSingleton INSTANCE = new StaticFieldSingleton();
    private StaticFieldSingleton() { // 私有化構造方法,防止外部實例化而破壞單例
        if (INSTANCE != null) { // 防止反射攻擊
            throw new UnsupportedOperationException();
        }
    }

    // 外部可調用StaticFieldSingleton.INSTANCE.doSomething()
    public void doSomething() {
        System.out.println("StaticFieldSingleton: do something like accessing resources");
    }
}

靜態工廠方法單例

示例代碼以下:日誌

public class StaticMethodSingleton {
    private static final StaticMethodSingleton INSTANCE = new StaticMethodSingleton(); // INSTANCE由private修飾
    private StaticMethodSingleton() {
        if (INSTANCE != null) { // 防止反射攻擊
            throw new UnsupportedOperationException();
        }
    }
    public static StaticMethodSingleton getInstance() {
        return INSTANCE;
    }

    // 外部可調用StaticFieldSingleton.getInstance().doSomething()
    public void doSomething() {
        System.out.println("StaticMethodSingleton: do something like accessing resources");
    }
}

靜態工廠方法比靜態公有域單例更具靈活性:code

  • 內部能夠改變單例實現方式,例如將即時加載改形成延時加載/懶加載,保持API不變。
  • 甚至能夠改變類是不是單例。例如業務場景有所改變,將原先的單例變成非單例,也能保持API不變。

延時加載

即時加載相對簡單,做爲主要推薦的單例模式。但在有些業務場景中,不但願單例被過早建立,而在真正使用的那刻才建立,即延時加載單例(也稱懶漢式)。此類場景有:對象

  • 建立實例的開銷很大,但訪問頻率卻很低
  • 單例的建立依賴於其餘資源的建立,爲保證數據完整性必須延遲建立。
  • ...

靜態內部類單例(推薦方式)

示例代碼以下:

public class StaticHolderSingleton {
    private static class SingletonHolder {
        private static final StaticHolderSingleton INSTANCE = new StaticHolderSingleton();
    }
    private StaticHolderSingleton() {}
    public static StaticHolderSingleton getInstance() {
        return SingletonHolder.INSTANCE;
    }

    // 外部可調用StaticHolderSingleton.getInstance().doSomething()
    public void doSomething() {
        System.out.println("StaticHolderSingleton: do something like accessing resources");
    }
}

靜態內部類單例有如下特色:

  • 只有當getInstance()方法被外部首次調用時,SingletonHolder類才被JVM加載和初始化,靜態屬性INSTANCE也跟着被初始化,從而達到延遲加載的目的。
  • JVM保證初始化SingletonHolder類時,具備線程安全性,所以不會增長任何性能成本和空間浪費。

雙重校驗鎖(DCL)單例

示例代碼以下:

public class DCLSingleton {
    private static volatile DCLSingleton instance; // volatile禁止指令重排序,並保證內存可見性
    private DCLSingleton() {}
    public static DCLSingleton getInstance() {
        if (instance == null) { // 此處判空旨在提升性能
            synchronized (DCLSingleton.class) {
                if (instance == null) {
                    instance = new DCLSingleton();
                }
            }
        }
        return instance;
    }

    // 外部可調用DCLSingleton.getInstance().doSomething()
    public void doSomething() {
        System.out.println("DCLSingleton: do something like accessing resources");
    }
}

DCL單例比較複雜,並且用到synchronized和volatile,性能有所損失。

破壞單例模式的方法

Java對象可經過new、克隆(clone)、反序列化(serialize)、反射(reflect)等方式建立。
經過私有化或不提供構造方法,可阻止外部經過new建立單例實例。其餘幾種建立方式則須要特別注意(枚舉單例不存在本節風險)。

克隆

java.lang.Obeject#clone()方法不會調用構造方法,而是直接從內存中拷貝內存區域。所以,單例類不能實現Cloneable接口。

反射

反射經過調用構造方法生成新的對象,可在構造方法中進行判斷,實例已建立時拋出異常,如StaticFieldSingleton所示。

反序列化

普通Java類反序列化時會經過反射調用類的默認構造方法來初始化對象。若是單例類實現java.io.Serializable接口, 就能夠經過反序列化破壞單例。
所以,單例類儘可能不要實現序列化接口。如若必須,能夠重寫反序列化方法readResolve(), 反序列化時直接返回相關單例對象:

public Object readResolve() {
    return instance;
}

單例 vs 靜態方法

單例:在一個JVM中只容許一個實例存在。單例經常是帶有狀態的,能夠攜帶更豐富的信息,使用場景更加普遍。

  • 單例是面向對象的
  • 有狀態的
  • 方法跟實例是相關的
  • 人爲保證線程安全
  • 能實現接口或者繼承一個超類

靜態方法: 對於不須要維護任何狀態,僅提供全局訪問方法的類,可將其實現爲更簡單的靜態方法類(如各類Uitls工具類),它的速度更快。

  • 靜態方法是面向過程的
  • 無狀態的
  • 方法跟實例是無關的
  • 自然線程安全
  • 靜態方法速度更快(其綁定在編譯期就進行)

業界實踐

  • java.lang.Runtime.getRuntime(JDK)
  • java.util.concurrent.TimeUnit(JDK)
  • 無數開源軟件

要點總結

  • 單例模式按加載時機可分爲即時加載延時加載兩種方式。
  • 即時加載有:枚舉類單例、靜態公有域單例和靜態工廠方法單例。
    • 推薦程度: 枚舉類單例 > 靜態工廠方法單例 > 靜態公有域單例。
    • 特例:若單例類必需要繼承某個超類,則不宜使用枚舉類單例。
  • 延時加載有:靜態內部類單例和雙重校驗鎖(DCL)單例。
    • 推薦靜態內部類單例。
    • 應避免使用雙重校驗鎖單例。
  • 若無特殊須要,優先使用即時加載模式的單例。
  • 對於一些無狀態的具備"惟一"特徵的類(如工具類),建議使用靜態方法實現。
相關文章
相關標籤/搜索