1. 程式人生 > >單例模式“雙重檢查鎖定Double-Checked Locking”執行緒安全問題

單例模式“雙重檢查鎖定Double-Checked Locking”執行緒安全問題

幾篇合集。

1 單例模式“雙重檢查鎖定Double-Checked Locking”執行緒安全問題

https://blog.csdn.net/wabiaozia/article/details/84723899

2 主題:用happen-before規則重新審視DCL

https://blog.csdn.net/wabiaozia/article/details/84727407

3 Double-checked locking: Clever, but broken

http://www.cs.umd.edu/~pugh/java/memoryModel/jsr-133-faq.html#dcl

4 Does the new memory model fix the "double-checked locking" problem?

https://blog.csdn.net/wabiaozia/article/details/84839566

注:

1 1和 3.4三篇寫的比較早,有些問題已經處理更新了。可以看文章下的一些評論。

2 volatile遮蔽指令重排序的語義在JDK1.5中才被完全修復,此前的JDK中及時將變數宣告為volatile,也仍然不能完全避免重排序所導致的問題(主要是volatile變數前後的程式碼仍然存在重排序問題),這點也是在JDK1.5之前的Java中無法安全使用DCL來實現單例模式的原因
3 在java5之前對final欄位的同步語義和其它變數沒有什麼區別,在java5中,final變數一旦在建構函式中設定完成(前提是在建構函式中沒有洩露this引用),其它執行緒必定會看到在建構函式中設定的值。而DCL的問題正好在於看到物件的成員變數的預設值,因此我們可以將LazySingleton的someField變數設定成final,這樣在java5中就能夠正確運行了。

以下是原文:

轉載請標明連線:https://blog.csdn.net/wabiaozia/article/details/84723899

譯文原連結:http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html

Signed byDavid Bacon (IBM Research) Joshua Bloch (Javasoft), Jeff Bogda, Cliff Click (Hotspot JVM project), Paul HaahrDoug Lea

Tom MayJan-Willem MaessenJeremy MansonJohn D. Mitchell (jGuru) Kelvin Nilsen, Bill PughEmin Gun Sirer

Double-Checked Locking is widely cited and used as an efficient method for implementing lazy initialization in a multithreaded environment.

Unfortunately, it will not work reliably in a platform independent way when implemented in Java, without additional synchronization. When implemented in other languages, such as C++, it depends on the memory model of the processor, the reorderings performed by the compiler and the interaction between the compiler and the synchronization library. Since none of these are specified in a language such as C++, little can be said about the situations in which it will work. Explicit memory barriers can be used to make it work in C++, but these barriers are not available in Java.

To first explain the desired behavior, consider the following code:

 

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

If this code was used in a multithreaded context, many things could go wrong. Most obviously, two or more Helper objects could be allocated. (We'll bring up other problems later). The fix to this is simply to synchronize the getHelper() method:

 

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

The code above performs synchronization every time getHelper() is called. The double-checked locking idiom tries to avoid synchronization after the helper is allocated:

 

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

Unfortunately, that code just does not work in the presence of either optimizing compilers or shared memory multiprocessors.

It doesn't work

There are lots of reasons it doesn't work. The first couple of reasons we'll describe are more obvious. After understanding those, you may be tempted to try to devise a way to "fix" the double-checked locking idiom. Your fixes will not work: there are more subtle reasons why your fix won't work. Understand those reasons, come up with a better fix, and it still won't work, because there are even more subtle reasons.

Lots of very smart people have spent lots of time looking at this. There is no way to make it work without requiring each thread that accesses the helper object to perform synchronization.

The first reason it doesn't work

The most obvious reason it doesn't work it that the writes that initialize the Helper object and the write to the helper field can be done or perceived out of order. Thus, a thread which invokes getHelper() could see a non-null reference to a helper object, but see the default values for fields of the helper object, rather than the values set in the constructor.

If the compiler inlines the call to the constructor, then the writes that initialize the object and the write to the helper field can be freely reordered if the compiler can prove that the constructor cannot throw an exception or perform synchronization.

Even if the compiler does not reorder those writes, on a multiprocessor the processor or the memory system may reorder those writes, as perceived by a thread running on another processor.

Doug Lea has written a more detailed description of compiler-based reorderings.

A test case showing that it doesn't work

Paul Jakubik found an example of a use of double-checked locking that did not work correctly. A slightly cleaned up version of that code is available here.

When run on a system using the Symantec JIT, it doesn't work. In particular, the Symantec JIT compiles

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

to the following (note that the Symantec JIT using a handle-based object allocation system).

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

As you can see, the assignment to singletons[i].reference is performed before the constructor for Singleton is called. This is completely legal under the existing Java memory model, and also legal in C and C++ (since neither of them have a memory model).

A fix that doesn't work

Given the explanation above, a number of people have suggested the following code:

 

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

This code puts construction of the Helper object inside an inner synchronized block. The intuitive idea here is that there should be a memory barrier at the point where synchronization is released, and that should prevent the reordering of the initialization of the Helper object and the assignment to the field helper.

Unfortunately, that intuition is absolutely wrong. The rules for synchronization don't work that way. The rule for a monitorexit (i.e., releasing synchronization) is that actions before the monitorexit must be performed before the monitor is released. However, there is no rule which says that actions after the monitorexit may not be done before the monitor is released. It is perfectly reasonable and legal for the compiler to move the assignment helper = h; inside the synchronized block, in which case we are back where we were previously. Many processors offer instructions that perform this kind of one-way memory barrier. Changing the semantics to require releasing a lock to be a full memory barrier would have performance penalties.

More fixes that don't work

There is something you can do to force the writer to perform a full bidirectional memory barrier. This is gross, inefficient, and is almost guaranteed not to work once the Java Memory Model is revised. Do not use this. In the interests of science, I've put a description of this technique on a separate page. Do not use it.

However, even with a full memory barrier being performed by the thread that initializes the helper object, it still doesn't work.

The problem is that on some systems, the thread which sees a non-null value for the helper field also needs to perform memory barriers.

Why? Because processors have their own locally cached copies of memory. On some processors, unless the processor performs a cache coherence instruction (e.g., a memory barrier), reads can be performed out of stale locally cached copies, even if other processors used memory barriers to force their writes into global memory.

I've created a separate web page with a discussion of how this can actually happen on an Alpha processor.

Is it worth the trouble?

For most applications, the cost of simply making the getHelper() method synchronized is not high. You should only consider this kind of detailed optimizations if you know that it is causing a substantial overhead for an application.

Very often, more high level cleverness, such as using the builtin mergesort rather than handling exchange sort (see the SPECJVM DB benchmark) will have much more impact.

Making it work for static singletons

If the singleton you are creating is static (i.e., there will only be one Helper created), as opposed to a property of another object (e.g., there will be one Helper for each Foo object, there is a simple and elegant solution.

Just define the singleton as a static field in a separate class. The semantics of Java guarantee that the field will not be initialized until the field is referenced, and that any thread which accesses the field will see all of the writes resulting from initializing that field.

 

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

It will work for 32-bit primitive values

Although the double-checked locking idiom cannot be used for references to objects, it can work for 32-bit primitive values (e.g., int's or float's). Note that it does not work for long's or double's, since unsynchronized reads/writes of 64-bit primitives are not guaranteed to be atomic.

 

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

In fact, assuming that the computeHashCode function always returned the same result and had no side effects (i.e., idempotent), you could even get rid of all of the synchronization.

 

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

Making it work with explicit memory barriers

It is possible to make the double checked locking pattern work if you have explicit memory barrier instructions. For example, if you are programming in C++, you can use the code from Doug Schmidt et al.'s book:

 

// C++ implementation with explicit memory barriers
// Should work on any platform, including DEC Alphas
// From "Patterns for Concurrent and Distributed Objects",
// by Doug Schmidt
template <class TYPE, class LOCK> TYPE *
Singleton<TYPE, LOCK>::instance (void) {
    // First check
    TYPE* tmp = instance_;
    // Insert the CPU-specific memory barrier instruction
    // to synchronize the cache lines on multi-processor.
    asm ("memoryBarrier");
    if (tmp == 0) {
        // Ensure serialization (guard
        // constructor acquires lock_).
        Guard<LOCK> guard (lock_);
        // Double check.
        tmp = instance_;
        if (tmp == 0) {
                tmp = new TYPE;
                // Insert the CPU-specific memory barrier instruction
                // to synchronize the cache lines on multi-processor.
                asm ("memoryBarrier");
                instance_ = tmp;
        }
    return tmp;
    }

Fixing Double-Checked Locking using Thread Local Storage

Alexander Terekhov ([email protected]) came up clever suggestion for implementing double checked locking using thread local storage. Each thread keeps a thread local flag to determine whether that thread has done the required synchronization.

  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);
         }
	}

The performance of this technique depends quite a bit on which JDK implementation you have. In Sun's 1.2 implementation, ThreadLocal's were very slow. They are significantly faster in 1.3, and are expected to be faster still in 1.4. Doug Lea analyzed the performance of some techniques for implementing lazy initialization.

Under the new Java Memory Model

As of JDK5, there is a new Java Memory Model and Thread specification.

Fixing Double-Checked Locking using Volatile

JDK5 and later extends the semantics for volatile so that the system will not allow a write of a volatile to be reordered with respect to any previous read or write, and a read of a volatile cannot be reordered with respect to any following read or write. See this entry in Jeremy Manson's blog for more details.

With this change, the Double-Checked Locking idiom can be made to work by declaring the helper field to be volatile. This does not work under JDK4 and earlier.

 

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

Double-Checked Locking Immutable Objects

If Helper is an immutable object, such that all of the fields of Helper are final, then double-checked locking will work without having to use volatile fields. The idea is that a reference to an immutable object (such as a String or an Integer) should behave in much the same way as an int or float; reading and writing references to immutable objects are atomic.

Descriptions of double-check idiom

 

以下是谷歌翻譯:

轉載請標明連線:https://blog.csdn.net/wabiaozia/article/details/84723899

簽名: David Bacon(IBM Research)Joshua Bloch(Javasoft), Jeff Bogda,Cliff Click(Hotspot JVM專案), Paul Haahr, Doug Lea, Tom May, Jan-Willem MaessenJeremy Manson, John D. Mitchell(jGuru) Kelvin Nilsen, Bill Pugh, Emin Gun Sirer

Double-Checked Locking被廣泛引用並用作在多執行緒環境中實現延遲初始化的有效方法。

遺憾的是,如果沒有額外的同步,它將無法以獨立於平臺的方式在Java中實現時可靠地工作。當用其他語言(如C ++)實現時,它取決於處理器的記憶體模型,編譯器執行的重新排序以及編譯器和同步庫之間的互動。由於這些都不是用C ++這樣的語言指定的,因此對它的工作情況幾乎沒有什麼可說的。可以使用顯式記憶體屏障使其在C ++中工作,但這些障礙在Java中不可用。

要首先解釋所需的行為,請考慮以下程式碼:

 

//單執行緒版本
class Foo { 
  private Helper helper = null;
  public Helper getHelper(){
    if(helper == null) 
        helper = new Helper();
    回報助手;
    }
  //其他功能和成員......
  }

如果在多執行緒上下文中使用此程式碼,很多事情可能會出錯。最明顯的是,可以分配兩個或更多 Helper物件。(我們稍後會提出其他問題)。解決這個問題只是為了同步getHelper()方法:

 

//更正多執行緒版本
class Foo { 
  private Helper helper = null;
  public synchronized Helper getHelper(){
    if(helper == null) 
        helper = new Helper();
    回報助手;
    }
  //其他功能和成員......
  }

上面的程式碼每次 呼叫getHelper()時都會執行同步。雙重檢查鎖定習慣用法在分配幫助程式後嘗試避免同步:

 

//破碎的多執行緒版本
//“Double-Checked Locking”成語
class Foo { 
  private Helper helper = null;
  public Helper getHelper(){
    if(helper == null) 
      synchronized(this){
        if(helper == null) 
          helper = new Helper();
      }    
    回報助手;
    }
  //其他功能和成員......
  }

不幸的是,該程式碼在優化編譯器或共享記憶體多處理器的存在下不起作用。

它不起作用

有很多原因它不起作用。我們將描述的前幾個原因更為明顯。在理解了這些之後,你可能會試圖設法一種“修復”雙重檢查鎖定習語的方法。您的修復程式將無法正常工作:您的修復程式無法正常工作的原因更為微妙。瞭解這些原因,提出更好的解決方案,但它仍然無法正常工作,因為還有更微妙的原因。

很多非常聰明的人花了很多時間看這個。有沒有辦法讓它無需訪問輔助物件進行同步每個執行緒工作。

它不起作用的第一個原因

最明顯的原因,它不工作,它認為該初始化寫操作助手物件,並在寫幫手場可以做或感覺不正常。因此,呼叫getHelper()的執行緒可以看到對輔助物件的非空引用,但是請參閱輔助物件的欄位的預設值,而不是建構函式中設定的值。

如果編譯器內聯對建構函式的呼叫,那麼如果編譯器可以證明建構函式不能丟擲異常或執行同步,則可以自由地重新排序初始化物件和寫入輔助物件欄位的寫入。

即使編譯器沒有重新排序這些寫入,在多處理器上,處理器或記憶體系統也可能重新排序這些寫入,正如在另一個處理器上執行的執行緒所感知的那樣。

Doug Lea撰寫了更詳細的基於編譯器的重新排序的描述

一個測試用例顯示它不起作用

保羅·雅庫比克發現了一個使用雙重檢查鎖定的例子,但沒有正常工作。此處提供了該程式碼的略微清理版本

在使用Symantec JIT的系統上執行時,它不起作用。特別是Symantec JIT編譯

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

以下(注意Symantec JIT使用基於控制代碼的物件分配系統)。

0206106A mov eax,0F97E78h
0206106F撥打01F6B210; 為...分配空間
                                                 ; 單身,返回結果在eax
02061074 mov dword ptr [ebp],eax; EBP是&singletons [i] .reference
                                                ; 在這裡儲存未構造的物件。
02061077 mov ecx,dword ptr [eax]; 取消引用控制代碼
                                                 ; 得到原始指標
02061079 mov dword ptr [ecx],100h; 接下來是4行
0206107F mov dword ptr [ecx + 4],200h; Singleton的內聯建構函式
02061086 mov dword ptr [ecx + 8],400h
0206108D mov dword ptr [ecx + 0Ch],0F84030h

如您所見,在 呼叫Singleton的建構函式之前執行對singletons [i] .reference的賦值。這在現有的Java記憶體模型下是完全合法的,並且在C和C ++中也是合法的(因為它們都沒有記憶體模型)。

修復不起作用

鑑於上述解釋,許多人建議使用以下程式碼:

 

//(仍然)破碎的多執行緒版本
//“Double-Checked Locking”成語
class Foo { 
  private Helper helper = null;
  public Helper getHelper(){
    if(helper == null){
      幫手h;
      synchronized(this){
        h =幫手;
        if(h == null) 
            synchronized(this){
              h =新助手();
            } //釋放內部同步鎖
        幫手= h;
        } 
      }    
    回報助手;
    }
  //其他功能和成員......
  }

此程式碼將Helper物件的構造放在內部同步塊中。這裡的直觀想法是,在釋放同步的位置應該有一個記憶體屏障,這應該可以防止重新排序Helper物件的初始化和對欄位助手的賦值。

不幸的是,這種直覺絕對是錯誤的。同步規則不起作用。monitorexit(即釋放同步)的規則是必須在監視器釋放之前執行monitorexit之前的操作。但是,沒有規則說明在監視器釋出之前可能無法執行monitorexit之後的操作。編譯器移動賦值helper = h是完全合理和合法的; 在synchronized塊中,在這種情況下,我們回到了之前的位置。許多處理器提供執行此類單向記憶體屏障的指令。將語義更改為要求將鎖定釋放為完整記憶體屏障會產生效能損失。

更多不適用的修復程式

您可以採取一些措施來強制編寫器執行完全雙向記憶體屏障。這是嚴重的,低效的,並且幾乎保證在修改Java記憶體模型後不起作用。不要用這個。為了科學的利益,我在一個單獨的頁面上對這種技術進行了描述。不要使用它。

但是,即使初始化輔助物件的執行緒執行完整的記憶體屏障,它仍然不起作用。

問題是在某些系統上,看到輔助欄位的非空值的執行緒也需要執行記憶體屏障。

為什麼?因為處理器有自己的本地快取記憶體副本。在一些處理器上,除非處理器執行快取記憶體一致性指令(例如,儲存器屏障),否則可以從陳舊的本地快取記憶體的副本執行讀取,即使其他處理器使用儲存器屏障來強制其寫入全域性儲存器。

我已經建立了一個單獨的網頁,討論瞭如何在Alpha處理器上實際發生這種情況。

這值得嗎?

對於大多數應用程式,簡單地使getHelper() 方法同步的成本並不高。如果您知道它會導致應用程式的大量開銷,那麼您應該只考慮這種詳細的優化。

通常,更高級別的聰明,例如使用內建mergesort而不是處理交換排序(請參閱SPECJVM DB基準測試)將產生更大的影響。

使它適用於靜態單例

如果您正在建立的單例是靜態的(即,只建立一個Helper),而不是另一個物件的屬性(例如,每個Foo物件將有一個Helper,則有一個簡單而優雅的解決方案。

只需將單例定義為單獨類中的靜態欄位。Java的語義保證在引用欄位之前不會初始化欄位,並且訪問該欄位的任何執行緒都將看到初始化該欄位所產生的所有寫入。

 

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

它適用於32位原始值

雖然雙重檢查的鎖定習慣用法不能用於物件的引用,但它可以用於32位原始值(例如,int或float)。請注意,它不適用於long或double,因為不保證64位原語的未同步讀/寫是原子的。

 

//為32位基元校正雙重檢查鎖定
class Foo { 
  private int cachedHashCode = 0;
  public int hashCode(){
    int h = cachedHashCode;
    if(h == 0) 
    synchronized(this){
      if(cachedHashCode!= 0)返回cachedHashCode;
      h = computeHashCode();
      cachedHashCode = h;
      }
    返回h;
    }
  //其他功能和成員......
  }

實際上,假設computeHashCode函式總是返回相同的結果並且沒有副作用(即冪等),您甚至可以擺脫所有同步。

 

//延遲初始化32位基元
//如果computeHashCode是冪等的,則是執行緒安全的
class Foo { 
  private int cachedHashCode = 0;
  public int hashCode(){
    int h = cachedHashCode;
    if(h == 0){
      h = computeHashCode();
      cachedHashCode = h;
      }
    返回h;
    }
  //其他功能和成員......
  }

使其與明確的記憶體障礙一起工作

如果您有明確的記憶體屏障指令,則可以使雙重檢查鎖定模式工作。例如,如果您使用C ++程式設計,則可以使用Doug Schmidt等人的書中的程式碼:

 

//具有顯式記憶體障礙的C ++實現
//應該可以在任何平臺上工作,包括DEC Alphas
//來自“併發和分散式物件的模式”,
// Doug Schmidt撰寫
template <class TYPE,class LOCK> TYPE *
Singleton <TYPE,LOCK> :: instance(void){
    //首先檢查
    TYPE * tmp = instance_;
    //插入CPU特定的記憶體屏障指令
    //同步多處理器上的快取行。
    asm(“memoryBarrier”);
    if(tmp == 0){
        //確保序列化(警衛
        //建構函式獲取lock_)。
        守衛<LOCK>後衛(lock_);
        // 再檢查一遍。
        tmp = instance_;
        if(tmp == 0){
                tmp = new TYPE;
                //插入CPU特定的記憶體屏障指令
                //同步多處理器上的快取行。
                asm(“memoryBarrier”);
                instance_ = tmp;
        }
    返回tmp;
    }

使用執行緒本地儲存修復雙重檢查鎖定

Alexander Terekhov([email protected])提出了使用執行緒本地儲存實現雙重檢查鎖定的聰明建議。每個執行緒都保留一個執行緒本地標誌,以確定該執行緒是否已完成所需的同步。

  class Foo {
	 / **如果perThreadInstance.get()返回一個非null值,則此執行緒
		已完成檢視初始化所需的同步
		幫手* /
         private final ThreadLocal perThreadInstance = new ThreadLocal();
         private Helper helper = null;
         public Helper getHelper(){
             if(perThreadInstance.get()== null)createHelper();
             回報助手;
         }
         private final void createHelper(){
             synchronized(this){
                 if(helper == null)
                     helper = new Helper();
             }
	     //任何非null值都可以作為引數
             perThreadInstance.set(perThreadInstance);
         }
	}

這種技術的效能在很大程度上取決於您擁有的JDK實現。在Sun的1.2實現中,ThreadLocal非常慢。它們在1.3中明顯更快,預計在1.4中更快。Doug Lea分析了實現延遲初始化的一些技術的效能

在新的Java記憶體模型下

從JDK5開始,有一個新的Java記憶體模型和執行緒規範

使用易失性修復雙重檢查鎖定

JDK5和更高版本擴充套件了volatile的語義,以便系統不允許對任何先前的讀或寫進行重新排序的volatile的寫入,並且對於任何後續的讀或寫,不能重新排序volatile的讀取。有關詳細資訊,請參閱 Jeremy Manson部落格中的此條目

通過這種改變,可以通過宣告輔助欄位是易失性來使雙重鎖定成語工作。這不起作用 JDK4和更早版本下。

 

//使用volatile的獲取/釋放語義
//在volatile的當前語義下斷開
  class Foo {
        private volatile Helper helper = null;
        public Helper getHelper(){
            if(helper == null){
                synchronized(this){
                    if(helper == null)
                        helper = new Helper();
                }
            }
            回報助手;
        }
    }

雙重檢查鎖定不可變物件

如果Helper是一個不可變物件,使得Helper的所有欄位都是最終的,那麼雙重檢查鎖定將無需使用volatile欄位即可工作。我們的想法是對不可變物件(如String或Integer)的引用應該與int或float的行為方式大致相同; 讀取和寫入對不可變物件的引用是原子的。

複述成語的描述