Java單例模式雙重檢查

// Single threaded version
class Foo {
    private Helper helper = null;
    public Helper getHelper() {
        if (helper == null) {
            helper = new Helper();
        }
        return helper;
    }
 
    // other functions and members...
}



這段在使用多線程的狀況下沒法正常工做。在多個線程同時調用getHelper()時,必需要獲取,不然,這些線程可能同時去建立對象,或者某個線程會獲得一個未徹底初始化的對象。 php

鎖能夠經過代價很高的同步來得到,就像下面的例子同樣。 html


// Correct but possibly expensive multithreaded version
class Foo {
    private Helper helper = null;
    public synchronized Helper getHelper() {
        if (helper == null) {
            helper = new Helper();
        }
        return helper;
    }
 
    // other functions and members...
}



只有getHelper()的第一次調用須要同步建立對象,建立以後getHelper()只是簡單的返回成員變量,而這裏是無需同步的。 因爲同步一個方法會下降100倍或更高的性能[2], 每次調用獲取和釋放鎖的開銷彷佛是能夠避免的:一旦初始化完成,獲取和釋放鎖就顯得很沒必要要。許多程序員一下面這種方式進行優化: java

  1. 檢查變量是否被初始化(不去得到鎖),若是已被初始化當即返回這個變量。
  2. 獲取鎖
  3. 第二次檢查變量是否已經被初始化:若是其餘線程曾獲取過鎖,那麼變量已被初始化,返回初始化的變量。
  4. 不然,初始化並返回變量。
// Broken multithreaded version
// "Double-Checked Locking" idiom
class Foo {
    private Helper helper = null;
    public Helper getHelper() {
        if (helper == null) {
            synchronized(this) {
                if (helper == null) {
                    helper = new Helper();
                }
            }
        }
        return helper;
    }
 
    // other functions and members...
}



直覺上,這個算法看起來像是該問題的有效解決方案。然而,這一技術還有許多須要避免的細微問題。例如,考慮下面的事件序列: 程序員

  1. 線程A發現變量沒有被初始化, 而後它獲取鎖並開始變量的初始化。
  2. 因爲某些編程語言的語義,編譯器生成的代碼容許在線程A執行完變量的初始化以前,更新變量並將其指向部分初始化的對象。
  3. 線程B發現共享變量已經被初始化,並返回變量。因爲線程B確信變量已被初始化,它沒有獲取鎖。若是在A完成初始化以前共享變量對B可見(這是因爲A沒有完成初始化或者由於一些初始化的值尚未穿過B使用的內存(緩存一致性)),程序極可能會崩潰。


J2SE 1.4或更早的版本中使用雙重檢查鎖有潛在的危險,有時會正常工做:區分正確實現和有小問題的實現是很困難的。取決於編譯器,線程的調度和其餘併發系統活動,不正確的實現雙重檢查鎖致使的異常結果可能會間歇性出現。重現異常是十分困難的。 算法

J2SE 5.0中,這一問題被修正了。volatile關鍵字保證多個線程能夠正確處理單件實例。[4]描述了這一新的語言特性: 編程

// Works with acquire/release semantics for volatile
// Broken under Java 1.4 and earlier semantics for volatile
class Foo {
    private volatile Helper helper = null;
    public Helper getHelper() {
        Helper result = helper;
        if (result == null) {
            synchronized(this) {
                result = helper;
                if (result == null) {
                    helper = result = new Helper();
                }
            }
        }
        return result;
    }
 
    // other functions and members...
}



注意局部變量result的使用看起來是沒必要要的。對於某些版本的Java虛擬機,這會使代碼提速25%,而對其餘的版本則無關痛癢。[3] 緩存

若是helper對象是靜態的(每一個類只有一個), 可使用雙重檢查鎖的替代模式惰性初始化模式[4]。查看[5] 上的列表16.6。 安全

// Correct lazy initialization in Java
@ThreadSafe
class Foo {
    private static class HelperHolder {
       public static Helper helper = new Helper();
    }
 
    public static Helper getHelper() {
        return HelperHolder.helper;
    }
}



這是由於內部類直到他們被引用時纔會加載。 多線程

Java 5中的final語義能夠不使用volatile關鍵字實現安全的建立對象:[6] 併發

public class FinalWrapper<T> {
    public final T value;
    public FinalWrapper(T value) {
        this.value = value;
    }
}
 
public class Foo {
   private FinalWrapper<Helper> helperWrapper = null;
 
   public Helper getHelper() {
      FinalWrapper<Helper> wrapper = helperWrapper;
 
      if (wrapper == null) {
          synchronized(this) {
              if (helperWrapper == null) {
                  helperWrapper = new FinalWrapper<Helper>(new Helper());
              }
              wrapper = helperWrapper;
          }
      }
      return wrapper.value;
   }
}



爲了正確性,局部變量wrapper 是必須的。這一實現的性能不必定比使用volatile 的性能更高。
相關文章
相關標籤/搜索