Java設計模式-單例模式(Singleton Pattern)

定義

單例模式是一個比較"簡單"的模式,其定義以下:java

保證一個類僅有一個實例,並提供一個訪問它的全局訪問點。git

或者數據庫

Ensure a class has only one instance, and provide a global point of access to it.設計模式

確保某一個類只有一個實例,並且自行實例化並向整個系統提供這個實例。安全

請注意"簡單"二字的雙引號,說它簡單它也簡單,可是要想用好、用對其實並不那麼簡單,爲何這麼說?多線程

  • 首先,單例模式的定義比較好理解,應用場景明確,實現思路比較簡單;
  • 其次,單例模式其實要考慮的因素不少,諸如延遲加載、線程安全以及破壞單例的狀況等等。也正是這些因素致使單例模式的實現方式多樣,且各有利弊

特色

  • 單例類只能有一個實例;
  • 單例類必須本身建立本身的惟一實例;
  • 單例類必須給全部其餘對象提供這一實例。

基本步驟

  1. 私有的靜態成員變量:在本類中建立惟一實例,使用靜態成員變量保存;爲保證安全性,私有化這個成員變量
  2. 私有的構造方法:避免其餘類能夠直接建立單例類的對象
  3. 公有的靜態方法:供其餘類獲取本類的惟一實例

考慮的因素

  • 延遲加載jvm

  • 線程安全ide

  • 破壞單例的狀況函數

    • 序列化工具

      若是Singleton類是可序列化的,僅僅在生聲明中加上implements Serializable是不夠的。爲了維護並保證Singleton,必須聲明全部實例域都是瞬時(transient)的,而且提供一個readResolve方法。不然,每次反序列化一個序列化的實例時,都會建立一個新的對象。

    • 反射

      受權的客戶端能夠經過反射來調用私有構造方法,藉助於AccessibleObject.setAccessible方法便可作到 。若是須要防範這種攻擊,請修改構造函數,使其在被要求建立第二個實例時拋出異常。

      private Singleton() { 
      		System.err.println("Singleton Constructor is invoked!");
      		if (singleton != null) {
      			System.err.println("實例已存在,沒法初始化!");
      			throw new UnsupportedOperationException("實例已存在,沒法初始化!");
      		}
      	}
      }
      複製代碼
    • 對象複製

      在Java中,對象默認是不能夠被複制的,若實現了Cloneable接口,並實現了clone方法,則能夠直接經過對象複製方式建立一個新對象,對象複製是不用調用類的構造函數,所以即便是私有的構造函數,對象仍然能夠被複制。在通常狀況下,類複製的狀況不須要考慮,不多會出現一個單例類會主動要求被複制的狀況,解決該問題的最好方法就是單例類不要實現Cloneable接口。

    • 類加載器

      若是單例由不一樣的類裝載器裝入,那便有可能存在多個單例類的實例。

實現方式

一、懶漢式

線程不安全(適用於單線程)
public class Singleton {
    private static Singleton singleton;

    private Singleton() {
    }

    public static Singleton getInstance() {
        if (singleton == null) {
            singleton = new Singleton();
        }
        return singleton;
    }
}
複製代碼
  • 優勢:延遲加載
  • 缺點:線程不安全,多線程環境下有可能產生多個實例

爲解決懶漢式"線程安全問題",能夠將getInstance()設置爲同步方法,因而就有了第二種實現方式:

線程安全
public class Singleton {
    private static Singleton singleton;

    private Singleton() {
    }

    public static synchronized Singleton getInstance() {
        if (singleton == null) {
            singleton = new Singleton();
        }
        return singleton;
    }
}
複製代碼
  • 優勢:延遲加載,而且線程安全
  • 缺點:效率很低,99%的狀況下實際上是不須要同步的

二、餓漢式

public class Singleton {
    private static Singleton singleton = new Singleton();

    private Singleton() {
    }

    public static Singleton getInstance() {
        return singleton;
    }
}
複製代碼
  • 優勢:線程安全,實現簡單
  • 缺點:沒有延遲加載,類加載的時候即完成初始化,可能在必定程度上形成內存空間的浪費

若是不是特別須要延遲加載的場景,能夠優先考慮餓漢式

三、雙重檢查鎖

public class Singleton {
    private static volatile Singleton singleton;

    private Singleton() {
    }

    public static Singleton getInstance() {
        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}
複製代碼
  • 優勢:延遲加載,線程安全,而且效率也很不錯

  • 缺點:實現相對複雜一點,JDK1.5之後才支持volatile

  • 說明

    • 將同步方法改成同步代碼塊
    • 第一個判空是爲了解決效率問題,不須要每次都進入同步代碼塊
    • synchronized (Singleton.class)是爲了解決線程安全問題
    • 第二個判空是避免產生多個實例
    • volatile修飾符是禁止指令重排序

    這裏針對volatile多說兩句,不少書上和網上的雙重檢查鎖實例都沒有加volatile,事實上這是不正確的

    首先,volatile的兩層含義:

    1. 內存可見性
    2. 禁止指令重排

    這裏咱們用到的主要是第二個語義。那麼什麼是指令重排序呢,就是指編譯器和處理器爲了優化程序性能而對指令序列進行排序的一種手段。簡單理解,就是編譯器對咱們的代碼進行了優化,在實際執行指令的的時候可能與咱們編寫的順序不一樣,只保證程序執行結果與源代碼相同,卻不保證明際指令的順序與源代碼相同。

    singleton = new Singleton();

    這段代碼在jvm執行時實際分爲三步:

    1. 在堆內存開闢一塊內存空間;
    2. 在堆內存實例化Singleton
    3. 把對象(singleton)指向堆內存空間

    因爲"指令重排"的優化,極可能執行步驟爲1-3-2,即:對象並無實例化完成但引用已是非空了,也就是在第二處判空的地方爲false,直接返回singleton——一個未完成實例化的對象引用。

    這裏涉及到Java內存模型、內存屏障等知識點,本文主要介紹單例模式,所以再也不贅述,有興趣的同窗能夠自行百度

四、靜態內部類

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

    private Singleton() {
    }

    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}
複製代碼

與餓漢式的區別是,靜態內部類SingletonHolder只有在getInstance()方法第一次調用的時候纔會被加載(實現了延遲加載效果)。

所以靜態內部類實現方式既能保證線程安全,也能保證單例的惟一性,同時也具備延遲加載特性

五、枚舉

public enum  Singleton {
    INSTANCE;
    public void doSomething() {
        System.out.println("doSomething");
    }
}
複製代碼

優勢:枚舉方式具備以上全部實現方式的優勢,同時還無償地提供了序列化機制,防止屢次實例化

缺點:JDK1.5之後才支持enum;普及度較前幾種方式不高

優勢

  • 因爲單例模式在內存中只有一個實例,減小了內存開支,特別是一個對象須要頻繁地建立、銷燬時,並且建立或銷燬時性能又沒法優化,單例模式的優點就很是明顯。
  • 因爲單例模式只生成一個實例,因此減小了系統的性能開銷,當一個對象的產生須要比較多的資源時,如讀取配置、產生其餘依賴對象時,則能夠經過在應用啓動時直接產生一個單例對象,而後用永久駐留內存的方式來解決(在Java EE中採用單例模式時須要注意JVM垃圾回收機制)。
  • 單例模式能夠避免對資源的多重佔用,例如一個寫文件動做,因爲只有一個實例存在內存中,避免對同一個資源文件的同時寫操做。
  • 單例模式能夠在系統設置全局的訪問點,優化和共享資源訪問,例如能夠設計一個單例類,負責全部數據表的映射處理。

缺點

  • 單例模式通常沒有接口,擴展很困難,若要擴展,除了修改代碼基本上沒有第二種途徑能夠實現。單例模式爲何不能增長接口呢?由於接口對單例模式是沒有任何意義的,它要求「自行實例化」,而且提供單一實例、接口或抽象類是不可能被實例化的。固然,在特殊狀況下,單例模式能夠實現接口、被繼承等,須要在系統開發中根據環境判斷。
  • 單例模式對測試是不利的。在並行開發環境中,若是單例模式沒有完成,是不能進行測試的,沒有接口也不能使用mock的方式虛擬一個對象。
  • 單例模式與單一職責原則有衝突。一個類應該只實現一個邏輯,而不關心它是不是單例的,是否是要單例取決於環境,單例模式把「要單例」和業務邏輯融合在一個類中。

使用場景

在一個系統中,要求一個類有且僅有一個對象,若是出現多個對象就會出現「不良反應」,能夠採用單例模式,具體的場景以下:

  • 要求生成惟一序列號的環境;
  • 在整個項目中須要一個共享訪問點或共享數據,例如一個Web頁面上的計數器,能夠不用把每次刷新都記錄到數據庫中,使用單例模式保持計數器的值,並確保是線程安全的;
  • 建立一個對象須要消耗的資源過多,如要訪問IO和數據庫等資源;
  • 須要定義大量的靜態常量和靜態方法(如工具類)的環境,能夠採用單例模式(固然,也能夠直接聲明爲static的方式)。

源碼地址:gitee.com/tianranll/j…

參考文獻:《設計模式之禪》、《Effective Java》

相關文章
相關標籤/搜索