【譯】能夠不要再使用Double-Checked Locking了

Double-Checked Locking方法被普遍的使用於實現多線程環境下單例模式的懶加載方式實現,不幸的是,在JAVA中,這種方式有可能不可以正常工做。在其餘語言環境中,如C++,依賴於處理器的內存模型、編譯器的重排序以及編譯器和同步庫之間的工做方式。因爲這些問題在C++中並不肯定,所以咱們不可以肯定具體的行爲。可是在C++中顯示的內存屏障是能夠被用來讓其正常工做的,而這些屏障在JAVA中又很差用。
html


1、Double-Checked Locking入門

首先來看看下面這段代碼咱們指望獲得的行爲:
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...
  }

這段代碼若是運行在多線程環境下,將會出現問題。很顯然的一個問題,兩個或者多個Helper對象將會被分配內存,其餘問題咱們會在後面提到,咱們先簡單的給方法加一個synchronized關鍵字。
緩存

// Correct 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方法的時候都要進行同步,下面的Double-Checked Locking方式避免了當Helper對象被實例化以後再次進行同步:
多線程

// 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...
  }

不幸的是,這段代碼在存在編譯優化或多處理器共享內存的狀況下不可以正常工做。ide


2、Double-Checked Locking不可以正常工做

爲何上文說Double-Checked Locking不可以正常工做有不少的緣由,咱們將會描述一對很顯而易見的緣由。經過理解存在的問題,咱們嘗試着去修復Double-Checked Locking存在的問題,然而咱們的修復可能並無用,咱們能夠一塊兒看看爲何沒有用,理解這些緣由,咱們去嘗試着尋找更好的方法,可能仍是沒有用,由於仍是存在一些微妙的緣由。函數


1)第一個不能正常工做的緣由

Double-Checked Locking不可以正常工做的一個很顯然的緣由是對helper屬性的寫指令和初始化Helper對象的指令可能被衝排序,所以當其餘線程再次調用getHelper方法的時候,將會獲得一個沒有被初始化完成的Helper對象,若是這個線程訪問了這個對象沒有被初始化的屬性,那麼就會出現位置錯誤。性能

咱們來看看對於下面這行代碼,在Symantec JIT編譯器環境下的指令重排序的例子:優化

singletons[i].reference = new Singleton();

下面是實際執行的代碼:ui

0206106A   mov         eax,0F97E78h
0206106F   call        01F6B210                  ; allocate space for
                                                 ; Singleton, return result in eax
02061074   mov         dword ptr [ebp],eax       ; EBP is &singletons[i].reference 
                                                ; store the unconstructed object here.
02061077   mov         ecx,dword ptr [eax]       ; dereference the handle to
                                                 ; get the raw pointer
02061079   mov         dword ptr [ecx],100h      ; Next 4 lines are
0206107F   mov         dword ptr [ecx+4],200h    ; Singleton's inlined constructor
02061086   mov         dword ptr [ecx+8],400h
0206108D   mov         dword ptr [ecx+0Ch],0F84030h

咱們能夠看到對於singletons[i].reference的賦值操做是在構造Singleton對象以前,這在當前的JAVA內存模型中是徹底合法的,在C和C++中也是合法的。
this


2)一種無用的修復

理解了上面的問題,有些同窗給出了下面的這段代碼,試圖避免問題:

// (Still) Broken multithreaded version
// "Double-Checked Locking" idiom
class Foo { 
  private Helper helper = null;
  public Helper getHelper() {
    if (helper == null) {
      Helper h;
      synchronized(this) {
        h = helper;
        if (h == null) 
            synchronized (this) {
              h = new Helper();
            } // release inner synchronization lock
        helper = h;
        } 
      }    
    return helper;
    }
  // other functions and members...
  }

上面的代碼將對象構造放在一個內部的synchronized塊裏面,直覺的想法是想經過synchronized釋放以後的屏障來避免問題,從而阻止對helper屬性的賦值和對Helper對象的構造的指令重排序。不幸的是,直覺是錯誤的。由於synchronization的規則能保證全部在monitorexit以前的動做都可以生效而並不包含在monitorexit以後的動做在monitorexit以前不生效。也就是咱們可以保證在退出內部同步塊以前Helper可以被實例化,h可以被複制,可是不能保證helper被賦值必定發生在退出同步塊以後,所以一樣會出現沒有被構造完的Helper實例被其餘線程引用並訪問。


3)其餘無用的修復

咱們能夠經過徹底雙向的內存屏障來強制行爲生效,這麼作是粗魯的,非高效的,而且幾乎能夠保證一旦JAVA內存模型被修訂,原有方式將不可以正常工做。因此,請不要這麼作。然而,即便經過徹底內存屏障,仍是不可以正常工做。問題是在一些系統上,線程對非空的helper屬性字段一樣須要內存屏障。爲何呢?由於處理器擁有本身的緩存,在一些處理器中,除非處理器執行緩存一致性指令,不然將有可能從緩存讀取錯誤內容,儘管其餘處理器將內容從緩存刷新到了主存。


4)至於搞這麼複雜麼?

在不少應用中,簡單的將getHelper方法同步開銷其實並不大,除非可以證實其餘優化方案確實可以爲應用帶來很多的性能提高。


3、使用靜態域

若是咱們正要建立的實例是static的,咱們有一種很簡單的方法,僅僅將單例靜態屬性字段在一個單獨的類中定義:

class HelperSingleton {
  static Helper singleton = new Helper();
  }

這麼作既保證的懶加載,又保證單例被引用的時候已經被構造完成。


4、Double-Checked Locking對32位原始類型有效

儘管Double-Checked Locking對對象引用類型無效,對於32位原始類型倒是有效的,值得注意的是對64位的long和double類型並非有效的,由於64爲的long和double不可以保證被原子地讀寫。

// Correct Double-Checked Locking for 32-bit primitives
class Foo { 
  private int cachedHashCode = 0;
  public int hashCode() {
    int h = cachedHashCode;
    if (h == 0) 
    synchronized(this) {
      if (cachedHashCode != 0) return cachedHashCode;
      h = computeHashCode();
      cachedHashCode = h;
      }
    return h;
    }
  // other functions and members...
  }

實際上,假設computeHashCode函數老是有固定的返回值,咱們能夠不使用同步塊:

// Lazy initialization 32-bit primitives
// Thread-safe if computeHashCode is idempotent
class Foo { 
  private int cachedHashCode = 0;
  public int hashCode() {
    int h = cachedHashCode;
    if (h == 0) {
      h = computeHashCode();
      cachedHashCode = h;
      }
    return h;
    }
  // other functions and members...
  }


5、利用ThreadLocal修復Double-Checked Locking問題

Alexander Terekhov提出了一個聰明的方法,經過ThreadLocal來實現Double-Checked Locking,每一個Thread保持一個local flag來標識當前線程是否已經進入過同步塊:

class Foo {
	 /** If perThreadInstance.get() returns a non-null value, this thread
		has done synchronization needed to see initialization
		of helper */
         private final ThreadLocal perThreadInstance = new ThreadLocal();
         private Helper helper = null;
         public Helper getHelper() {
             if (perThreadInstance.get() == null) createHelper();
             return helper;
         }
         private final void createHelper() {
             synchronized(this) {
                 if (helper == null)
                     helper = new Helper();
             }
	     // Any non-null value would do as the argument here
             perThreadInstance.set(perThreadInstance);
         }
	}

這種方式的性能取決於JDK版本,在Sun公司的JDK1.2版本中,ThreadLocal是很慢的,在1.3版本以後變得很是快了。


6、在新的JAVA內存模型中

在JDK1.5或者更晚的版本中,擴展了volatile的語義,使得咱們能夠經過將helper屬性字段設置爲volatile來修復Double-Checked的問題:

// Works with acquire/release semantics for volatile
// Broken under current semantics for volatile
  class Foo {
        private volatile Helper helper = null;
        public Helper getHelper() {
            if (helper == null) {
                synchronized(this) {
                    if (helper == null)
                        helper = new Helper();
                }
            }
            return helper;
        }
    }

   

7、使用不變實例

還有一種方法是講單例對象變爲不可變對象,如全部字段都聲明爲final或者相似String類或Integer類這種。


本文由博主翻譯改編自:http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html 歡迎轉載,若有內容錯誤,還請多多包涵。

相關文章
相關標籤/搜索