單例模式與線程安全問題淺析
近期看到到Struts1與Struts2的比較。說Struts1的控制器是單例的,線程不安全的;Struts2的多例的,不存在線程不安全的問題。之後又想到了之前自己用過的HttpHandler。。。
這些類。好像單例的線程安全問題確實是隨處可見的。
可是僅僅是知道這個是不安全的,也沒有認真分析過。接下來就細致分析下。
一,改動單例模式代碼
首先我先寫一段單例類的代碼:
/** * @ClassName: Sigleton * @Description: 單例類 * @author 水田 * @date 2015年12月19日 上午10:12:55 */ public class Sigleton { private static Sigleton sigleton; private Sigleton() { // put the initMethod for this class }; public static Sigleton getInstance() { // in this demo ,we use "Lazy-load Singleton" if (sigleton == null) { sigleton = new Sigleton(); } return sigleton; } }
這裏我使用的是延遲載入,不管是使用延遲載入還是以下的餓漢式:
public class Sigleton {
private static final Sigleton sigleton=new Sigleton();
private Sigleton() {
// put the initMethod for this class
};
public static Sigleton getInstance() {
return sigleton;
}
}
這兩種情況使用哪一種。要依據實際情況來推斷:究竟我是要在此類還沒有使用之前進行初始化。還是要在用到它去拿它的時候才初始化。還要看你的實際應用場景。比方說,我這個類超級大,這時候,你部署好了之後。就把它New了。然後放在內存中,十年八年的沒人用。這不是浪費麽?(情況舉的比較極端,就是這個意思吧。只是你要是硬要跟我說。內存啥的越來越不值錢。或者爺有的是錢買內存。我也沒辦法!
僅僅能送你句,怪不得你沒有女朋友!
)
細致分析這兩種方法,然後看看哪裏存在線程不安全的因素。
先來瞅瞅第一種,lazy-load方式:
在調用getInstance的時候。先推斷,是不是已經被New過了。假設沒,那麽我new,完了之後返回。想象下,多線程。當在一個線程內。運行到if (sigleton == null),還有一個線程內,好巧啊。也運行到這裏。
然後兩個線程同一時候推斷發現還沒這個東西,然後各自new一個。
破壞了我的單實例的原則。
相比另外一種直接new的方式。這樣的方法顯然是不安全的。可是,假設我要用到這樣的lazy-load方式,就要對它進行改進了。
簡單改進:
public static synchronized Sigleton getInstance()
加個keyword。
可是這樣的方法還是不好,產生問題的僅僅有sigleton = new Sigleton();如今我鎖定了整個方法。有點兒多余了。再改下:
public final Sigleton getInstance() {
// in this demo ,we use "Lazy-load Singleton"
if (sigleton == null) {
synchronized (this) {
sigleton = new Sigleton();
}
}
return sigleton;
}
然後改完了之後。我們再從邏輯上看下是不是有漏洞:
還是剛才的問題。倆線程,同一時候運行到if判空的時候,第一個線程由於調度原因,進入同步方法,運行了new操作,第二個線程判空完了之後,進不去,還等在同步方法外面。第一個線程出了同步方法,第二個線程進入了同步方法,又new了一個對象。。
。
。貌似確實有邏輯樓棟,再改下:
public final Sigleton getInstance() {
// in this demo ,we use "Lazy-load Singleton"
if (sigleton == null) {
synchronized (this) {
if (sigleton == null){
sigleton = new Sigleton();
}
}
}
return sigleton;
}
當第二個線程進入同步方法之後,要不要新new一個對象,還要推斷下。
二,After雙重檢查
上面的寫法一方面實現了Lazy-Load,還有一個方面也做到了並發度非常好的線程安全,一切看上非常完美。
可是二次檢查自身會存在比較隱蔽的問題,查了Peter Haggar在DeveloperWorks上的一篇文章,對二次檢查的解釋非常的具體:
“雙重檢查鎖定背後的理論是完美的。不幸地是,現實全然不同。
雙重檢查鎖定的問題是:並不能保證它會在單處理器或多處理器計算機上順利運行。雙重檢查鎖定失敗的問題並不歸咎於 JVM 中的實現 bug,而是歸咎於 Java 平臺內存模型。內存模型同意所謂的“無序寫入”。這也是這些習語失敗的一個主要原因。”
使用二次檢查的方法也不是全然安全的,原因是 java 平臺內存模型中同意所謂的“無序寫入”會導致二次檢查失敗,所以使用二次檢查的想法也行不通了。
Peter Haggar在最後提出這樣的觀點:“不管以何種形式。都不應使用雙重檢查鎖定,由於您不能保證它在不論什麽 JVM 實現上都能順利運行。”
問題在哪裏?
假設線程A運行到了推斷對象為空,於是線程A運行去初始化這個對象,但初始化是須要耗費時間的,可是這個對象的地址事實上已經存在了。
此時線程B也運行到了推斷不為空。於是直接跳到後面去得到了這個對象。可是,這個對象還沒有被完整的初始化。得到一個沒有初始化全然的對象有什麽用。!
關於這個Double-Checked Lock的討論有非常多。眼下公認這是一個Anti-Pattern,不推薦使用!
(from 網友)
三。怎樣安全+單例使用
首先說明下。餓漢模式是線程安全的。可是在某些情況下。比方,我們不得不使用lazy-load方式,能夠考慮以下方法:
1。使用volatilekeyword
private volatile static Sigleton sigleton;
有些人覺得使用 volatile 的原因是可見性。也就是能夠保證線程在本地不會存有instance 的副本,每次都是去主內存中讀取。
但事實上是不正確的。使用 volatile 的主要原因是其還有一個特性:禁止指令重排序優化。也就是說。在volatile 變量的賦值操作後面會有一個內存屏障(生成的匯編代碼上)。讀操作不會被重排序到內存屏障之前。從「先行發生原則」的角度理解的話,就是對於一個 volatile變量的寫操作都先行發生於後面對這個變量的讀操作(這裏的“後面”是時間上的先後順序)。
可是特別註意在 Java 5 曾經的版本號使用了volatile 的雙檢鎖還是有問題的。其原因是 Java 5 曾經的 JMM (Java 內存模型)是存在缺陷的,即時將變量聲明成 volatile也不能全然避免重排序。主要是 volatile 變量前後的代碼仍然存在重排序問題。這個 volatile 屏蔽重排序的問題在 Java 5中才得以修復,所以在這之後才幹夠放心使用 volatile。
2,Initialization on Demand Holder
public class Sigleton {
private static class SigletonHolder {
private static final Sigleton INSTANCE = new Sigleton();
}
private Sigleton() {
};
public static final Sigleton getInstance() {
return SigletonHolder.INSTANCE;
}
}
在使用sigleton時候。SigletonHolder會被初始化。可是裏面的INSTANCE卻不會,僅僅有當我們調用getInstance方法的時候。才會去new。
既沒有高大上的keyword,邏輯上也好理解。
細致分析。感覺還是蠻多問題的~
單例模式與線程安全問題淺析