1. 程式人生 > >Java記憶體模型FAQ(十一)新的記憶體模型是否修復了雙重鎖檢查問題?

Java記憶體模型FAQ(十一)新的記憶體模型是否修復了雙重鎖檢查問題?

臭名昭著的雙重鎖檢查(也叫多執行緒單例模式)是一個騙人的把戲,它用來支援lazy初始化,同時避免過度使用同步。在非常早的JVM中,同步非常慢,開發人員非常希望刪掉它。雙重鎖檢查程式碼如下:

// double-checked-locking - don't do this!

private static Something instance = null;

public Something getInstance() {
  if (instance == null) {
    synchronized (this) {
      if (instance == null)
        instance = new Something();
    }
  }
  return instance;
}

這看起來好像非常聰明——在公用程式碼中避免了同步。這段程式碼只有一個問題 —— 它不能正常工作。為什麼呢?最明顯的原因是,初始化例項的寫入操作和例項欄位的寫入操作能夠被編譯器或者緩衝區重排序,重排序可能會導致返回部分構造的一些東西。就是我們讀取到了一個沒有初始化的物件。這段程式碼還有很多其他的錯誤,以及為什麼對這段程式碼的演算法修正是錯誤的。在舊的java記憶體模型下沒有辦法修復它。更多深入的資訊可參見:Double-checkedlocking: Clever but broken and The “DoubleChecked Locking is broken” declaration

許多人認為使用volatile關鍵字能夠消除雙重鎖檢查模式的問題。在1.5的JVM之前,volatile並不能保證這段程式碼能夠正常工作(因環境而定)。在新的記憶體模型下,例項欄位使用volatile可以解決雙重鎖檢查的問題,因為在構造執行緒來初始化一些東西和讀取執行緒返回它的值之間有happens-before關係。

然後,對於喜歡使用雙重鎖檢查的人來說(我們真的希望沒有人這樣做),仍然不是好訊息。雙重鎖檢查的重點是為了避免過度使用同步導致效能問題。從java1.0開始,不僅同步會有昂貴的效能開銷,而且在新的記憶體模型下,使用volatile的效能開銷也有所上升,幾乎達到了和同步一樣的效能開銷。因此,使用雙重鎖檢查來實現單例模式仍然不是一個好的選擇。(修訂—在大多數平臺下,volatile效能開銷還是比較低的)。

使用IODH來實現多執行緒模式下的單例會更易讀:

private static class LazySomethingHolder {
  public static Something something = new Something();
}

public static Something getInstance() {
  return LazySomethingHolder.something;
}

這段程式碼是正確的,因為初始化是由static欄位來保證的。如果一個欄位設定在static初始化中,對其他訪問這個類的執行緒來說是是能正確的保證它的可見性的。

原文

Does the new memory model fix the “double-checked locking” problem?

The (infamous) double-checked locking idiom (also called the multithreaded singleton pattern) is a trick designed to support lazy initialization while avoiding the overhead of synchronization. In very early JVMs, synchronization was slow, and developers were eager to remove it — perhaps too eager. The double-checked locking idiom looks like this:

// double-checked-locking - don't do this!

private static Something instance = null;

public Something getInstance() {
  if (instance == null) {
    synchronized (this) {
      if (instance == null)
        instance = new Something();
    }
  }
  return instance;
}

This looks awfully clever — the synchronization is avoided on the common code path. There’s only one problem with it — it doesn’t work. Why not? The most obvious reason is that the writes which initialize instance and the write to the instance field can be reordered by the compiler or the cache, which would have the effect of returning what appears to be a partially constructed Something. The result would be that we read an uninitialized object. There are lots of other reasons why this is wrong, and why algorithmic corrections to it are wrong. There is no way to fix it using the old Java memory model. More in-depth information can be found at Double-checked locking: Clever, but broken and The “Double Checked Locking is broken” declaration

Many people assumed that the use of the volatile keyword would eliminate the problems that arise when trying to use the double-checked-locking pattern. In JVMs prior to 1.5, volatile would not ensure that it worked (your mileage may vary). Under the new memory model, making the instance field volatile will “fix” the problems with double-checked locking, because then there will be a happens-before relationship between the initialization of the Something by the constructing thread and the return of its value by the thread that reads it.

However, for fans of double-checked locking (and we really hope there are none left), the news is still not good. The whole point of double-checked locking was to avoid the performance overhead of synchronization. Not only has brief synchronization gotten a LOT less expensive since the Java 1.0 days, but under the new memory model, the performance cost of using volatile goes up, almost to the level of the cost of synchronization. So there’s still no good reason to use double-checked-locking. Redacted — volatiles are cheap on most platforms.

Instead, use the Initialization On Demand Holder idiom, which is thread-safe and a lot easier to understand:

private static class LazySomethingHolder {
  public static Something something = new Something();
}

public static Something getInstance() {
  return LazySomethingHolder.something;
}

This code is guaranteed to be correct because of the initialization guarantees for static fields; if a field is set in a static initializer, it is guaranteed to be made visible, correctly, to any thread that accesses that class.