單例模式(Singleton Pattern)

前言

:smile: 按照 001 篇講的,之後的每一個模式都將按照:模式名稱、問題、解決方案以及效果這幾個主要的要素研究。
java

  • 學習難度::smirk::smirk::smirk::smirk::smirk:
  • 使用頻率::wink::wink::wink::wink::wink:

開始吧git

模式名稱

中文:單例模式

English: Singleton Pattern

含義:單例對象的類必須保證只有一個實例存在。github

問題

什麼時候使用

當咱們須要統一管理資源,共享資源的時候就須要使用單例模式。保障數據庫

場景:設計模式

  1. Windows的Task(任務管理器) 就是很經典的單例模式。一臺電腦正常狀況下只能打開一個任務管理器。
  2. 網站的計數器,通常也採用單例模式實現,不然難以同步。
  3. 應用程序的日誌應用,通常都纔會用單例模式實現,這一般是因爲共享的日誌文件一直處於打開狀態,由於只能有一個實例去操做,不然內容很差追加。
  4. Web應用的配置對象的讀取,通常也應用單例模式,這個是因爲配置文件是共享的資源。
  5. 數據庫鏈接池的設計通常也是採用單例模式,由於數據庫鏈接是一種數據庫資源。數據庫軟件系統中使用數據庫鏈接池,主要是節省打開或者關閉數據庫鏈接所引發的效率損耗,這種效率上的損耗仍是很是昂貴的,由於何用單例模式來維護,就能夠大大下降這種損耗。
  6. 多線程的線程池的設計通常也是採用單例模式,這是因爲線程池要方便對池中的線程進行控制。
  7. 操做系統的文件系統,也是大的單例模式實現的具體例子,一個操做系統只能有一個文件系統。
  8. HttpApplication 也是單例的典型應用。熟悉ASP.Net(IIS)的整個請求生命週期的人應該知道HttpApplication也是單例模式,全部的HttpModule都共享一個HttpApplication實例.

存在的問題

  • 優勢
    • 單例模式提供了惟一實例的受控訪問,由於單例模式封裝了他的惟一實例,因此他能夠嚴格控制客戶怎樣以及什麼時候訪問它。
    • 因爲系統中只存在一個資源,對於一些須要頻繁建立和銷燬的對象單例模式能夠提升性能。
    • 容許可變數目的實例。基於單例模式咱們能夠進行擴展,使用與單例模式類似的方法來來得到指定個數的對象實例,既節省系統資源,又解決了單例對象共享過多有損性能的問題。
  • 缺點
    • 因爲單例模式沒有抽象層,所以單例類的擴展有很大的困難。
    • 單例類的職責太重,在必定程度上違背了‘單一職責的原則’。由於單例類既承擔了工廠的角色,提供了工廠方法,又充當了產品的角色,包含了一些業務方法,將產品的建立和產品自己的功能融合到一塊兒。
    • 如今不少面相對象語言的運行環境都提供了自動垃圾回收的技術,所以,若是實例化的共享對象長時間不被利用,系統會認爲他是垃圾,自動銷燬回收。下次使用時再從新實例化,這將致使共享的單例對象狀態的丟失。

請帶着問題找答案:stuck_out_tongue_winking_eye:。安全

解決方案

來個UML圖先多線程

Singleton UML
Singleton UML


( Singleton UML )


科普一下:什麼叫懶漢模式,什麼叫餓漢模式函數

  • 懶漢模式 --> 特色是懶,不用的時候我就睡覺(不實例化)。
  • 餓漢模式 --> 特色是餓,一上來我就要吃(實例化)。

最簡單的實現方法

// 懶漢,你不調用getInstance() 就不會實例化
public class Singleton {
    private static Singleton instance;
    public static Singleton getInstance() {
         if (instance == null) {
             instance = new Singleton();
         }
         return instance;
     }
}複製代碼

解析:這是一個最簡單的實現方法。經過 getInstance() 獲取 Singleton 這個類的實例。instance == null 的狀況下 new 一個實例。不爲空就返回這個實例。能夠說,這個方法最適合教學,一眼就能看的很明白什麼是單例。

BUT , 想想,我是否是在外面也能 經過 new Singleton() 來建立一個實例 ?那都是多個實例了。還怎麼單例?

So ,咱們要開始進階了。脫離學生手法,開始進階。post

// 懶漢,你不調用getInstance() 就不會實例化
public class Singleton {
    private static Singleton instance;
    public static Singleton getInstance() {
         if (instance == null) {
             instance = new Singleton();
         }
         return instance;
     }
     // add 1.1 begin (將構造方法私有化)
     private Singleton() {
     }
     // add 1.1 end
}複製代碼

通過 v1.1 版本的改造。發如今外面再也 new 不出來 Singleton 的實例。:stuck_out_tongue: 我學會了?給你個眼神:smirk:

咱們再想:兩個線程幾乎同時調用 getInstance() 第一個進入的線程判斷 instance 爲空,開始走這一行 instance = new Singleton();。,注意,是開始走這一行,並未完成實例化! 此時第二個線程也走到 if (instance == null)此時判斷也爲空。那麼 這兩個線程都會獲得一個新的實例,那麼就產生兩個實例。那麼還怎麼單例?性能

以上爲懶漢模式 -- (線程不安全)

懶漢模式 - 線程安全

爲了解決 上面那個方法在多線程下使用不安全的問題。咱們再次進階。
此次吊了�這從線程安全了!

我反手就給你一段代碼

// 懶漢,你不調用getInstance() 就不會實例化
public class Singleton {
    private static Singleton instance;
    // modif synchronized
    public static synchronized Singleton getInstance() {
         if (instance == null) {
             instance = new Singleton();
         }
         return instance;
     }
     // add 1.1 begin (將構造方法私有化)
     private Singleton() {
     }
     // add 1.1 end
}複製代碼

看,加一個synchronized同步,來一個要等着。等裏面的代碼執行完了,第二個線程再進去。第二個再進去的時候,instance就不是空了,就又能單例了。��


Too young。這樣搞的話,咱們每次進來均可能要同步一下。多數時候咱們並非兩個或多個線程同時來訪問,咱們並不須要去同步。這樣作其實形成了沒必要要的開銷。有木有更好的方法呢?


Double Checked locking pattern 【 雙重檢驗鎖 】



我反手又是一串代碼(關鍵代碼)

只貼上關鍵代碼。纔好

public static Singleton getInstance() {
    if (instance == null) {               //Single Checked
        synchronized (Singleton.class) {
            if (instance == null) {       //Double Checked
                instance = new Singleton();
            }
        }
    }
    return instance ;
}複製代碼

看到沒,你想到這樣搞了嗎? 先檢測再同步。Single Checked足以應付多數檢測。當一個以上的線程同時訪問時就用上了Double Checked防止多線程下重複建立實例。 �即便咱們這麼想到這麼吊的方法,仍是不行。。。爲啥?

爲啥? instance = new Singleton() 這個不是原子操做。當咱們 new 的時候 JVM 大概作了這些事:

  1. instance分配內存
  2. 調用 Singleton的構造函數,來初始化成員變量
  3. instance對象指向分配的內存空間(執行完這步 instance 就不爲 null 了)

可是在 JVM 的即時編譯器中存在指令重排序的優化。也就是說上面的第二步和第三步的順序是不能保證的,最終的執行順序多是 1-2-3 也多是 1-3-2。若是是後者,則在 3 執行完畢、2 未執行以前,被線程二搶佔了,這時 instance 已是非 null 了(但卻沒有初始化),因此線程二會直接返回 instance,而後使用,而後瓜熟蒂落地報錯。

解決方法也很簡單:加上 volatile 就能夠了。使用 volatile 的主要緣由是其一個特性:禁止指令重排序優化

private volatile static Singleton instance; //聲明成 volatile複製代碼

注意: 在 Java 5 之前的版本使用了 volatile 的雙檢鎖仍是有問題的。其緣由是 Java 5 之前的 JMM (Java 內存模型)是存在缺陷的,即時將變量聲明成 volatile 也不能徹底避免重排序,主要是 volatile 變量先後的代碼仍然存在重排序問題。這個 volatile 屏蔽重排序的問題在 Java 5 中才得以修復,因此在這以後才能夠放心使用 volatile。


註解
原子操做:
若是這個操做所處的層(layer)的更高層不能發現其內部實現與結構,那麼這個操做是一個原子(atomic)操做。
原子操做能夠是一個步驟,也能夠是多個操做步驟,可是其順序不能夠被打亂,也不能夠被切割而只執行其中的一部分。
將整個操做視做一個總體是原子性的核心特徵。

這個方法解決了上面全部的不安全因素,可是!在 Java 5 之前的版本上跑卻仍是會有問題,因此,這個也不是最好的方法。。。

餓漢來了

public class Singleton {  
    private static Singleton instance = new Singleton();
    private Singleton (){}
    public static Singleton getInstance() {
      return instance;
    }複製代碼

是這樣的,由於單例的實例 instance 被聲明稱 staticfinal 了,在第一次加載類到內存中就會被實例化。因此建立實例自己是線程安全的。一上來就加載,因此是餓漢。

缺點:太着急加載。有時候咱們想加點料也不給機會。有時候咱們建立實例須要依賴參數或者配置文件。這樣就不能使用餓漢模式。�

怎麼辦?

內部靜態類

先瞅代碼

public class Singleton {
    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }
    private Singleton() {}
    public static final Singleton getInstance(int a) {
        return SingletonHolder.INSTANCE;
    }
}複製代碼

因爲 SingletonHolder 是私有的,除了 getInstance() 以外沒有辦法訪問它,所以它是懶漢式的;同時讀取實例的時候不會進行同步,沒有性能缺陷;也不依賴 JDK 版本。能完美應對多數場景。若是你以爲寫着麻煩,其實還有這一種很簡單的寫法。好像不太經常使用。就是下面這個枚舉

枚舉單例

《Effective Java》:單元素的枚舉類型已經成爲實現Singleton的最佳方法

public enum Singleton {
     //理解爲 public static final Singleton INSTANCE;
     INSTANCE;
 }複製代碼

實例化:Singleton.INSTANCE 就是這麼簡單。可是單例這樣用的人我以爲仍是很少。猜測是你們對枚舉了解不太多吧。若是看到枚舉這個方式一臉懵B,就看看枚舉相關的知識。反正我一開始也是一臉**

效果

以上介紹了:

  • 簡單寫法(入門)
  • 懶漢模式線程安全
  • 雙重檢驗鎖(DLC)
  • 餓漢模式
  • 內部靜態類
  • 枚舉

幾種方法不重要。最重要的知道什麼是單例模式,爲啥用單例。甚至不在代碼中使用,工做、生活、學習、遊戲中充滿單例思想。掌握這個思想以及解決辦法,生活會很精彩��

再說一下:《Effective Java》推薦 DLC 和枚舉,那些明顯有問題的寫法就不要用了。那些寫法都是用來教學理解單例的。


更新中:
1. 什麼是設計模式
2. 單例模式
3. 簡單工廠模式
GitHub彙總:這裏老是最新的
看完給個star鼓勵一下

相關文章
相關標籤/搜索