1. 程式人生 > >單例模式的雙重檢測

單例模式的雙重檢測

單例模式是設計模式中比較常見簡單的一種,典型雙重檢測寫法如下:

public class SingletonClass { 

  private volatile static SingletonClass instance = null; 

  public static SingletonClass getInstance() { 
    if (instance == null) { 
      synchronized (SingletonClass.class) { 
        if(instance == null) { 
          instance 
= new SingletonClass(); } } } return instance; } private SingletonClass() { } }

接下來對該寫法進行分析,為何這樣寫?

一、為何要同步:

多執行緒情況下,若是A執行緒呼叫getInstance,發現instance為null,那麼它會開始建立例項,如果此時CPU發生時間片切換,執行緒B開始執行,呼叫getInstance,發現instance也null(因為A並沒有建立物件),然後B建立物件,然後切換到A,A因為已經檢測過了,不會再檢測了,A也會去建立物件,兩個物件,單例失敗。因此要同步。

 

二、同步為何不用 public synchronized static SingletonClass getInstance(),也就是說為何不同步這個方法,而要同步下面的語句:

因為synchronized修飾的同步塊可是要比一般的程式碼段慢上幾倍,如果經常呼叫getInstance,那麼效能問題就得考慮了。

 

三、最外層為何要有if (instance == null)判斷:

因為我們在分析二中,發現依舊存在著效能問題,也就是說,只要getInstance方法被呼叫,那麼就會執行同步這個操作,於是我們加個判斷,當instance沒有被例項化的時候,也就是需要去例項化的時候才去同步。

 

四、instance為何要有volatile 修飾:

這個問題就涉及到了編譯原理,所謂編譯,就是把原始碼“翻譯”成目的碼——大多數是指機器程式碼——的過程。針對Java,它的目的碼不是本地機器程式碼,而是虛擬機器程式碼。編譯原理裡面有一個很重要的內容是編譯器優化。所謂編譯器優化是指,在不改變原來語義的情況下,通過調整語句順序,來讓程式執行的更快。這個過程成為reorder。

JVM實現可以自由的進行編譯器優化。而我們建立變數的步驟:

1、申請一塊記憶體,呼叫構造方法進行初始化。

2、分配一個指標指向這塊記憶體。

而這兩個操作,JVM並沒有規定誰在前誰在後,那麼就存在這種情況:執行緒A開始建立SingletonClass的例項,此時執行緒B呼叫了getInstance()方法,首先判斷instance是否為null。按照我們上面所說的記憶體模型,A已經把instance指向了那塊記憶體,只是還沒有呼叫構造方法,因此B檢測到instance不為null,於是直接把instance返回了——問題出現了,儘管instance不為null,但它並沒有構造完成,就像一套房子已經給了你鑰匙,但你並不能住進去,因為裡面還沒有收拾。此時,如果B在A將instance構造完成之前就是用了這個例項,程式就會出現錯誤了。

在JDK 5之後,Java使用了新的記憶體模型。volatile關鍵字有了明確的語義——在JDK1.5之前,volatile是個關鍵字,但是並沒有明確的規定其用途——被volatile修飾的寫變數不能和之前的讀寫程式碼調整,讀變數不能和之後的讀寫程式碼調整!因此,只要我們簡單的把instance加上volatile關鍵字就可以了。

 

轉載請標明出處https://www.cnblogs.com/tangZH/p/10031337.html 

參考連結http://blog.51cto.com/devbean/203501