單例模式的雙重檢測
單例模式是設計模式中比較常見簡單的一種,典型雙重檢測寫法如下:
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關鍵字就可以了。