相信面向對象程序員都對單例模式比較熟悉,而對於單例模式卻有着各種各樣的寫法,今天我專門針對一種稱為雙重加鎖的寫法進行分析。我們先來看下這種寫法。
/** * 單例雙重加鎖Demo * */ public class DoubleCheckLock { private static DoubleCheckLock instance ; private DoubleCheckLock(){ } public static DoubleCheckLock getInstance(){ if(instance == null){ synchronized (DoubleCheckLock.class) { if(instance == null) instance = new DoubleCheckLock() ; } } return instance; } }
這種寫法相信很多人都見過,但是你認為這種寫法是正確的嗎?或者更准確的來說,這種寫法在並發的環境下是否還能表現出正確的行為呢。
之所以有這種所謂的雙重加鎖,一方面是因為延遲初始化可以提高性能,另一方面通過使用內置鎖sychronized來防止並發,其原理是首先檢查是否在沒有同步的情況下進行了初始化,如果沒有的話,在進行同步,然后再次檢查是否對其(instance)進行了初始化,如果沒有那么則初始化DoubleCheckLock。
這種寫法表面看起來既提高了性能,又保證了線程安全。但實際上卻並不是如此,我只從線程安全上來分析這種寫法的對錯。
在這,首先應該注意的是使用內置鎖加鎖的是DoubleCheckLock.class,並不是instance,也就是說沒有在instance實現同步,那么在這種情況下,當有兩個線程同時進行到synchronized代碼塊時,只有一個線程可以進入,然后初始化了instance,但是這僅僅只能保證的是兩個線程在訪問上的獨占性,也就是說兩個線程在此一定是一先一后進行訪問,但是不能保證的是instance的內存可見性,原因很簡單,因為同步的對象並不是instance,而是DoubleCheckLock.class(可以保證內存可見性)。不能保證內存可見性的后果就是當第一個線程初始化instance之后,第二個線程並不能馬上看見instance被初始化,或者更准確的來說,第二個線程看到的可能只是被部分構造的instance。因此,這種造成的后果是第二個線程讀取到了錯誤的instance的狀態,有可能instance會被再次實例化。
那么如何解決這個問題呢,最簡單的方式是對instance加上關鍵詞volatile,volatile可以保證變量的內存可見性,同時volatile同步的消耗也非常小,這么做到話,可以保證線程安全。
上述解決問題的方式固然是可以,但是實質上我感覺很繁瑣其代碼閱讀效果也不好,就單例而言,我推薦一下的寫法。
public class Single { private Single(){} private static class SingleHolder{ public static Single instance = new Single(); } public static Single getInstance(){ return SingleHolder.instance; } }
這種寫法相對而言比較簡單,而且處理了兩個問題:1.線程安全問題。2.延遲初始化(初始化在調用getInstance的時候才會去靜態內部類中初始化instance)。而且相對而言,有着更加良好的代碼可讀性。
對於雙重加鎖的這種寫法就先分析到這,等后面說到Happens-Before之后我會再來分下下雙重加鎖。