天天看點

double & operator[](int i)_深入解析單例模式之懶漢模式---Double-Check及volatile關鍵字單例模式--懶漢模式Demo單例模式--懶漢模式中的Double-Check的由來為什麼屬性要用volatile關鍵字最後

導讀:在日常開發中對單例設計模式的應用十分常見,而看似簡單小巧的設計模式其内部卻蘊含着豐富的知識點。單例的建立方式有很多如懶漢模式和餓漢模式等、不同的語言又有不同的實作方式,但其本質的思想為:保證一個類僅有一個執行個體,并提供一個通路它的全局通路點。接下來,本文主要讨論在Java中單例模式之懶漢模式的實作及其double-check的思想、volatile關鍵字的作用。

單例模式--懶漢模式Demo

  • 将構造方法私有化,防止外界利用new建立此類的執行個體
  • 提供一個本類執行個體的唯一全局通路點
  • 不事先執行個體化對象,對象instance屬性加上volatile關鍵字
  • 應用Double-check和同步鎖保證了擷取執行個體時線程安全和效率
/** * 懶漢模式 */public class SingletonTest {    private volatile static SingletonTest instance = null;    /**     * 将構造方法私有化,防止外部使用new建立執行個體     */    private SingletonTest() {    }    /**     * 提供一個擷取執行個體的方法     */    public static SingletonTest getInstance() {               if (instance == null) {            synchronized (SingletonTest.class) {                if (instance == null) {                    instance = new SingletonTest();                }            }        }        return instance;    }}
           

單例模式--懶漢模式中的Double-Check的由來

懶漢模式中應用Double-Check,Double-Check的目的是保證擷取執行個體時線程安全和效率。下面我們對SingletonTest類内部getInstance()方法進行分解,來演變Double-Check出現的過程。

double & operator[](int i)_深入解析單例模式之懶漢模式---Double-Check及volatile關鍵字單例模式--懶漢模式Demo單例模式--懶漢模式中的Double-Check的由來為什麼屬性要用volatile關鍵字最後

1、隻有一個Check

修改getInstance()為隻使用一個Check,通過該Check判斷執行個體對象是否存在,如果不存在則進行執行個體化。但是在多線程的情況下,多個線程并發調用SingletonTest靜态類執行getInstance()方法會導緻出現重複執行個體化和覆寫問題。如:線程A先經過了Check但是還沒來得及進行執行個體化,這時線程B也就順利通過了Check,那麼這就會導緻兩個線程都執行那段執行個體化的代碼,重複執行個體化。

public static SingletonTest getInstance() {        if (instance == null) {                    instance = new SingletonTest();        }        return instance;    }
           

2、同步鎖保證線程安全

造成上面這一問題根本原因在于:一段代碼在多線程的情況下存在被多個線程同時執行的情況。那麼解決這一問題的思路則是上同步鎖,同步鎖保證了同一時刻隻能有一個線程執行同步代碼塊中的代碼。

public static SingletonTest getInstance() {            return instance;    }
           

3、第二個Check使性能提升

由于加上了同步鎖,當有一個線程取得鎖時其他線程就會處于阻塞的狀态。雖然保證了線程安全,但是卻對效率造成了影響。如:對象已經執行個體化了,其他線程想取得對象都要排隊等待取得同步鎖後才能取得對象。通過增加多一層Check,則可以優雅地解決此問題

getInstance
           

4、為什麼要進行兩次判斷對象是否存在?

通過上面的過程我們得知Double-Check的演變:先進行一次Check--再加一層同步鎖--再加一層Check,由内到外。那麼有一個疑惑,最外層的Check是判斷對象是否存在,裡層的Check也是判斷對象是否存在。從作用來看起來似乎備援,那可否去掉裡層的判斷呢? 下面我們通過試驗來得到該問題的答案。

4.1、把裡層Check幹掉

假設線程A和B并發調用getInstance(),線程A先通過了Check并取得鎖但是還沒來得及進行執行個體化。這時線程B也就可以順利地通過Check并處于一個阻塞的狀态。

double & operator[](int i)_深入解析單例模式之懶漢模式---Double-Check及volatile關鍵字單例模式--懶漢模式Demo單例模式--懶漢模式中的Double-Check的由來為什麼屬性要用volatile關鍵字最後

加上同步鎖雖然保證了線程安全,但是仍存在着重複執行個體化和覆寫問題。如:當線程A執行完畢後釋放鎖,線程B就會取得鎖并執行同步塊裡的代碼。這時如果沒有加上第二次Check判斷執行個體化對象是否存在則會導緻線程B也進行執行個體化操作,結果導緻發生重複執行個體化和覆寫。

double & operator[](int i)_深入解析單例模式之懶漢模式---Double-Check及volatile關鍵字單例模式--懶漢模式Demo單例模式--懶漢模式中的Double-Check的由來為什麼屬性要用volatile關鍵字最後

是以,Double-Check對于懶漢模式十分重要,既保證了線程安全也保證了執行效率。

double & operator[](int i)_深入解析單例模式之懶漢模式---Double-Check及volatile關鍵字單例模式--懶漢模式Demo單例模式--懶漢模式中的Double-Check的由來為什麼屬性要用volatile關鍵字最後

為什麼屬性要用volatile關鍵字

單例模式另外一個關注點在于instance屬性加上了volatile關鍵字,要了解為什麼加上該關鍵字,首先需要了解下面這行簡單代碼内部究竟做了什麼事情

instance = new SingletonTest();//在new一個對象過程按分為以下幾個步驟
           
  1. 配置設定記憶體空間
  2. 初始化執行個體
  3. 設定instance指向剛配置設定的位址

而問題處在于第2和3步會出現重排序(重排序是指編譯器和處理器為了優化程式性能而對指令序列進行重新排序的一種手段)既可能會出現重排序 從1-2-3 排序變為1-3-2,那麼在多線程的情況下就會導緻出現問題。

double & operator[](int i)_深入解析單例模式之懶漢模式---Double-Check及volatile關鍵字單例模式--懶漢模式Demo單例模式--懶漢模式中的Double-Check的由來為什麼屬性要用volatile關鍵字最後

下面用線程A和線程B舉例

線程A執行到new SingletonTest(),開始初始化執行個體對象,由于存在指令重排序,這次new操作先執行了3把引用指派了,還沒有執行2初始化執行個體。這時時間片結束了,切換到線程B執行,線程B調用new SingletonTest()後發現引用不等于null,便直接傳回引用位址了,這樣會導緻線程B通路到了一個還未初始化的對象。

解決方案:通過給instance增加volatile關鍵字之後,就保證new不會發生指令重排序,為被volatile關鍵字修飾的變量是被禁止重排序的。

最後

自此關于單例模式-懶漢模式的讨論結束。其中涉及到了Double-Check思想、同步鎖解決線程安全問題、及使用volatile關鍵字解決重排序問題。單例模式看似簡單,但其内部蘊含的知識點非常豐富,隻有仔細和深入了解才能更好更安全地應用它。