單例模式-雙重檢查鎖定的警告

01. 單例模式

1. 介紹與比較

咱們常常看到的單例模式,按加載時機能夠分爲:餓漢方式和懶漢方式;按實現的方式,有:synchronized修飾方法、雙重檢查加鎖,內部類方式和枚舉方式等等。另外還有一種經過Map容器來管理單例的方式。
html

2. 雙重檢查鎖定的Bug

今天寫了一個工具類,以單例的形式持有內部具體處理類的引用。java

public class LogProcessorUtils {

    private static LogProcessorInterface logProcessor = null;

    private LogProcessorUtils() {
    }

    public static String processLog2Json(String s) {

        // 保證logProcessor單例
        if (logProcessor == null) {
            synchronized (LogProcessorUtils.class) {
                if (logProcessor == null) {
                    logProcessor = new LogProcessorImpl();
                }
            }
        }

        return logProcessor.process(s);
    }
}

可是,在FindBugs插件中爆出bug。如下是網頁中的介紹:web

  • DC: Possible double check of field (DC_DOUBLECHECK)

This method may contain an instance of double-checked locking. This idiom is not correct according to the semantics of the Java memory model. For more information, see the web page http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html.函數

大體意思是,以雙重檢查鎖定實現的單例,在理論是正確的,可是實際上,由於Java內存模型的緣由,可能形成錯誤,不推薦使用。工具

網上查閱了資料,參考《http://blog.csdn.net/chenchaofuck1/article/details/51702129.net

解釋以下:插件

public static Singleton getInstance(){
    if (instance == null){
        synchronized(Singleton.class) {  //1
          if (instance == null)          //2
            instance = new Singleton();  //3
        }
    }
    return instance;
}

雙重檢查鎖定背後的理論是:在 //2 處的第二次檢查使(如清單 3 中那樣)建立兩個不一樣的 Singleton 對象成爲不可能。假設有下列事件序列:
線程 1 進入 getInstance() 方法。線程

  1. 因爲 instance 爲 null,線程 1 在 //1 處進入 synchronized 塊。
  2. 線程 1 被線程 2 預佔。
  3. 線程 2 進入 getInstance() 方法。
  4. 因爲 instance 仍舊爲 null,線程 2 試圖獲取 //1 處的鎖。然而,因爲線程 1 持有該鎖,線程 2 在 //1 處阻塞。
  5. 線程 2 被線程 1 預佔。
  6. 線程 1 執行,因爲在 //2 處實例仍舊爲 null,線程 1 還建立一個 Singleton 對象並將其引用賦值給 instance。
  7. 線程 1 退出 synchronized 塊並從 getInstance() 方法返回實例。
  8. 線程 1 被線程 2 預佔。
  9. 線程 2 獲取 //1 處的鎖並檢查 instance 是否爲 null。
  10. 因爲 instance 是非 null 的,並無建立第二個 Singleton 對象,由線程 1 建立的對象被返回。

雙重檢查鎖定背後的理論是完美的。不幸地是,現實徹底不一樣。雙重檢查鎖定的問題是:並不能保證它會在單處理器或多處理器計算機上順利運行。
雙重檢查鎖定失敗的問題並不歸咎於 JVM 中的實現 bug,而是歸咎於 Java 平臺內存模型。內存模型容許所謂的「無序寫入」,這也是這些習語失敗的一個主要緣由。
code

無序寫入

爲解釋該問題,須要從新考察上述清單 4 中的 //3 行。此行代碼建立了一個 Singleton 對象並初始化變量 instance 來引用此對象。這行代碼的問題是:在 Singleton 構造函數體執行以前,變量 instance 可能成爲非 null 的。orm

什麼?這一說法可能讓您始料未及,但事實確實如此。在解釋這個現象如何發生前,請先暫時接受這一事實,咱們先來考察一下雙重檢查鎖定是如何被破壞的。假設清單 4 中代碼執行如下事件序列:

  1. 線程 1 進入 getInstance() 方法。
  1. 因爲 instance 爲 null,線程 1 在 //1 處進入 synchronized 塊。
  2. 線程 1 前進到 //3 處,但在構造函數執行以前,使實例成爲非 null。
  3. 線程 1 被線程 2 預佔。
  4. 線程 2 檢查實例是否爲 null。由於實例不爲 null,線程 2 將 instance 引用返回給一個構造完整但部分初始化了的 Singleton對象。
  5. 線程 2 被線程 1 預佔。
  6. 線程 1 經過運行 Singleton 對象的構造函數並將引用返回給它,來完成對該對象的初始化。
    此事件序列發生在線程 2 返回一個還沒有執行構造函數的對象的時候。

爲展現此事件的發生狀況,假設爲代碼行 instance =new Singleton(); 執行了下列僞代碼: instance =new Singleton();

mem = allocate();             //Allocate memory for Singleton object.
instance = mem;               //Note that instance is now non-null, but
                          //has not been initialized.
ctorSingleton(instance);      //Invoke constructor for Singleton passing
                          //instance.

這段僞代碼不只是可能的,並且是一些 JIT 編譯器上真實發生的。執行的順序是顛倒的,但鑑於當前的內存模型,這也是容許發生的。JIT 編譯器的這一行爲使雙重檢查鎖定的問題只不過是一次學術實踐而已。

爲說明這一狀況,假設有清單 5 中的代碼。它包含一個剝離版的 getInstance() 方法。我已經刪除了「雙重檢查性」以簡化咱們對生成的彙編代碼(清單 6)的回顧。咱們只關心 JIT 編譯器如何編譯 instance=new Singleton(); 代碼。此外,我提供了一個簡單的構造函數來明確說明彙編代碼中該構造函數的運行狀況。

3. 靜態內部類實現

public class LogProcessorUtils {
    private LogProcessorUtils() {
    }

    /**
     * 保證logProcessor單例
     */
    private static class ProcessorSingletonHolder {
        private final static LogProcessorInterface logProcessor = new LogProcessorImpl();
    }

    public static String processLog2Json(String s) {
        return ProcessorSingletonHolder.logProcessor.process(s);
    }
}

以後,FindBugs警告消失。

相關文章
相關標籤/搜索