Java 併發系列(二):DCL — Double Check Lock

1. DCL 的目的

Double Check Lock 是多線程環境下爲提升延遲初始化效率而被普遍使用的一種方式。咱們經常會使用延遲初始化,以下降服務啓動時間。html

/**
 * code 1.1
 */
@NotThreadSafe
public class Client {
  private LazyInitClass instance ;
  public LazyInitClass getInstance() {
    if(instance == null)
      instance = new LazyInitClass("LazyInitClassFieldName") ;
    return instance ;
  }
}

代碼java

上面的代碼是典型的延遲初始化的例子。當上面的例子暴露在多線程環境下時,便會出現各類問題。最明顯的錯誤:方法會返回多個 LazyInitClass 對象。git

/**
 * code 1.2
 */
@NotThreadSafe
public class Client {
    private LazyInitClass instance ;

    public synchronized LazyInitClass getInstance() {
        if(instance == null)
            instance = new LazyInitClass("LazyInitClassFieldName") ;

        return instance ;
    }
}

代碼github

上面的代碼在方法層面使用了 synchronized 關鍵字,每次調用 getInstance 方法都進行同步,的確能夠有效避免多線程環境下屢次調用 getInstance 獲得不一樣的 LazyInitClass 對象。但當 instance 初始化完成後,同步便沒有了意義。同步則成爲影響 getInstance 性能的關鍵。有沒有一種方法,能夠在初始化時進行正確的同步,初始化完成後又避免同步呢?因而 DCL 出現了。segmentfault

/**
 * code 1.3
 */
@NotThreadSafe
public class Client {
    private LazyInitClass instance ;

    public LazyInitClass getInstance() {
        if(instance == null){
            synchronized(this){
                if(instance == null){
                    instance = new LazyInitClass("LazyInitClassFieldName") ;
                }
            }
        }
        return instance ;
    }
}

代碼安全

很不幸,上述代碼在編譯器優化、多處理器共享內存的狀況下,並不能正常工做。多線程

LazyInitClass 代碼以下:併發

/**
 * code 1.4
 */
@NotThreadSafe
public class LazyInitClass {
    private String lazyInitClassField ;
    public LazyInitClass(String lazyInitClassField) {
        this.lazyInitClassField = lazyInitClassField ;
    }
}

代碼性能

2. DCL 存在的問題

LazyInitClass 實例寫入 instance field,與 LazyInitClass 對象內部 lazyInitClassField 對象的初始化兩步操做將會出現有序性問題。(詳細的有序性描述能夠閱讀上一篇文章:《Java 併發系列(一):多線程三大特性》優化

具體表現爲:某一線程調用 getInstance 方法後,將獲得一個非空的 instance 對象,但卻只能看到 lazyInitClassField 的默認值,即:lazyInitClassField 爲空字符串,而非構造方法中傳入的LazyInitClassFieldName。

3. 使 DCL 正常工做

3.1 JDK 1.3 之後(包含 JDK 1.3)的解決方案

/**
 * code 3.1
 */
@ThreadSafe
class Client {
         private final ThreadLocal perThreadInstance = new ThreadLocal();
         private LazyInitClass instance ;
         public LazyInitClass getInstance() {
             if (perThreadInstance.get() == null) createInstance();
             return instance;
         }
         private void createInstance() {
             synchronized(this) {
                 if (instance == null)
                     instance = new LazyInitClass("LazyInitClassFieldName");
             }
             perThreadInstance.set(perThreadInstance);
         }
    }

代碼

3.2 JDK 1.5 之後(包含 JDK 1.5)的解決方案

從 JDK5 開始,Java Memory Model 升級,volatile 關鍵字即可以保證可見性與有序性。

要使 DCL 正常工做,多了一種更爲方便的解決方案:

/**
 * code 3.2
 */
@ThreadSafe
public class Client {
    private volatile LazyInitClass instance ;

    public LazyInitClass getInstance() {
        if(instance == null){
            synchronized(this){
                if(instance == null){
                    instance = new LazyInitClass("LazyInitClassFieldName") ;
                }
            }
        }
        return instance ;
    }
}

代碼

3.3 JDK 1.3 之前(不包含 JDK 1.3)的解決方案

因爲 JDK1.2 版本,ThreadLocal 很是慢,因此 JDK 1.2 並不推薦使用 ThreadLocal 解決 DCL 問題。因此 JDK1.3 版本之前,DCL 並無解決方案。

3.4 不可變對象

/**
 * code 3.4
 */
@ThreadSafe
public class ImmutableLazyInitClass {
    private final String lazyInitClassField ;
    public ImmutableLazyInitClass(String lazyInitClassField) {
        this.lazyInitClassField = lazyInitClassField ;
    }
}

代碼

若是 LazyInitClass 對象是不可變對象,則不使用 volatile 關鍵字 DCL 也能正常工做(code 1.3 所示)。這是由 Java 內存模型中,final 域的特殊語義保證的:final 域能確保初始化過程的安全性,從而能夠不受限制地訪問不可變對象,並在共享這些對象時無須同步。

4. DCL 的替代方案

/**
 * code 4.1
 */
@ThreadSafe
public class Client {

    private static class LazyInitClassHolder {
      static LazyInitClass singleton = new LazyInitClass("LazyInitClassFieldName");
  }

    public static LazyInitClass getInstance() {
        return LazyInitClassHolder.singleton ;
    }
}

代碼

這種方式被稱爲延遲初始化佔位類模式,由 Java 語義保證:只有調用了 getInstance 方法後,LazyInitClassHolder.singleton 纔會被初始化。因此此方式能完美替代 DCL。

5. 總結

DCL 的使用方式已經被普遍廢棄。DCL 之因此出現是由於無競爭同步的執行速度很慢,以及 JVM 啓動很慢。但這兩個問題已經不復存在,於是它並非一種高效的優化措施。延遲初始化佔位類模式能帶來相同的優點,並更容易理解。

6. 參考資料

相關文章
相關標籤/搜索