1. 程式人生 > >單例模式與線程安全問題淺析

單例模式與線程安全問題淺析

ati 多線程 con data 非常完美 賦值 return span author



近期看到到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。


2Initialization 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,邏輯上也好理解。




細致分析。感覺還是蠻多問題的~






單例模式與線程安全問題淺析