單例模式是設計模式中比較常見簡單的一種,典型雙重檢測寫法如下: 接下來對該寫法進行分析,為何這樣寫? 一、為何要同步: 多線程情況下,若是A線程調用getInstance,發現instance為null,那麼它會開始創建實例,如果此時CPU發生時間片切換,線程B開始執行,調用getInstance, ...
單例模式是設計模式中比較常見簡單的一種,典型雙重檢測寫法如下:
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