java 雙重檢查加鎖弊端

http://blog.csdn.net/axman/article/details/1089196

Java是在語言級提供對線程的支持,所以Java的內存模型分爲主存儲器和工作存儲器.

[Main memory]主存儲器就是實例所在的存儲區域,所有實例本身都被放在主存儲器中,當然這
句話本身就說明了實例的字段也在主存儲器中,主存儲器被實例的所有線程所共有.

[working memory] 工作存儲器當然就是每個線程所專有的工作區域,當然其中有它們共有的
主存儲器中的一些必要的如實例字段等數據的COPY

當然你千萬要知道Java是運行在虛擬系統上的,我們說的主存儲器和工作存儲器都是在物理內存
中虛擬出來的.是在JVM內部而言的.

在JLS中,對字段的存取被規定爲read/write,use/assign,lock/unlock 六個最小的操作(action)

對於取而言,當一個線程一次訪問實例字段時,首先從主存儲器複製該字段的值到工作存儲器,
然後線程引用該值.

但是如果同一線程多次訪問該字段,是否每次都是從主存儲器複製到工作存儲器再引用,這由具體
的jvm環境決定.

簡單說有可能不從主存儲器中複製而直接引用工作區的COPY

同樣對於存,如果一個線程一次賦值實例字段,那麼會在工作存儲器中進行,然後由jvm決定什麼時
候映射到主存儲器.
而同一個線程如果多次反覆對實例字段賦值,那麼有可能只對工作存儲器的COPY進行,只把最後的
結果同步到主存儲器,當然也有可能是每次都把工作存儲器和主存儲器同步的.這也由具體的JVM決
定.

所以如果多個線程反覆對同一實例字段存取,就有可能一個線程對這個字段的改變沒有及時反映給
其它線程,因爲至少必須被從工作存儲器同步到主存儲器才能其它線程知道,而在線程專有的工作
存儲器中的值其它線程是不可訪問的.

所以boolean isInterrupted = false;這一句有可能一個線程中斷後在這個線程工作的範圍內已經
設爲true,但還沒有立即被映射到主存儲器時,其它線程還不知道.

同樣,對於double check,我們來看它的問題:
在java與模式一書中,作者這對個問題的說明根本沒有正確地理解.而且他一再說明是錯誤的例子
其實是正確的.

public MyObject{
    private static MyObect obj;
    public static getInstance(){
        if(obj == null){
            synchronized(MyObj.class){
                if(obj == null)
                    obj = new MyObject();
            }
        }
        return obj;
    }
}

這個例子根本不會存在問題.因爲線程的同步保證了不會在同步塊外部發生多線程調,而在同步塊中
只有一個線程能執行,其賦值的結果在離開同步塊時會強制映射到主工作區,也就是對obj的賦值一定
會在主工作區反映出來.所以作者舉這個例子說明他根本沒有理解爲什麼double check是不安全的.

我們來看下面的例子:
public MyObject{
    private static MyObect obj;
    private Date d = new Data();
    public Data getD(){return this.d;}
    public static MyObect  getInstance(){
        if(obj == null){
            synchronized(MyObect .class){
                if(obj == null)
                    obj = new MyObject();//這裏
            }
        }
        return obj;
    }
}
一個線程A運行到"這裏"時,對於A的工作區中,肯定已經產生一個MyObect對象,而且這時這個對象已經
完成了Data d.現在線程A調用時間到,執行權被切換到另一個線程B來執行,會有什麼問題呢?

如果obj不爲null,線程B獲得了一個obj,但可能obj.getD()卻還沒有初始化.

爲什麼?既然obj已經可見了(線程A還沒有離開同步塊),而d卻不可見呢?
如果d不可見,那麼obj也應該爲null啊?線程B應該等待A釋放同步塊啊?

事實上,對於"這裏"這條語句,線程A還沒有離開同步塊.
因爲沒有"離開同步塊"這個條件,線程a的工作區沒有強制與主存儲器同步,這時工作區中有兩個字段
obj,d 到底先把誰同步到主存儲區,沒有條件限制,雖然在線程A的工作區obj和d都是完整的,但有JSL
沒有強制不允許先把obj映射到主存儲區,如果哪個jvm實現按它的優化方案先把工作存儲器中的obj
同步到主存儲器了,這時正好線程B獲取了,而d卻沒有同步過去,那麼線程B就獲取了obj的引用卻找不能
obj.getD();

這個問題是否真的會發生?

我個人的意見,從理論上是會發生的,所以如果是設計銀行交易系統你就沒有必要爲提高一些性能而放
棄安全性,而如果只是一般的應用,發生這種情況的機率本來就非常發生也不會有太多的危害,我認爲
可以不去介意.比如獲取數據庫連結,即使獲取不到也不過讓訪問者再刷新一次重新查詢而已.事實上有
可能幾天,幾個月,一年都不會發生一次.知道小行星有可能會撞地球的,但你不要太在意而影響你正常
的生活.如果你不是做銀行交易系統的你不要太擔心,而大多數JVM實現本身就會保證這種安全的.也就 
在把obj同步到主存儲器會先同步d,但萬一發生相反的順序,你就無權責備你所用的JVM.只是這種萬一
機會不是很多.


當然如果是貪婪式調用,靜態實例的初始化由ClassLoader經由[Thread Safe]來完成,當然就不會有這
個問題了:
public MyObject{
    private static MyObect obj = new MyObject();
    private Date d = new Data();
    public Data getD(){return this.d;}
    public static getInstance(){
        return obj;
    }
}


那麼如何保證實例字段能在工作存儲區能被即時映,下一節我們來講最不常用的關鍵字

 

對於涉及對象初始化的DCL,從JAVA1.5以後雖然可以用volatile來修補,但已經沒有任何意義。因爲

 

比它更好的模式lazy initialization hoder可以達到相同的作用而更容易理解:

 

 

 

public MyObject{

 

 

 

    private static class instanceHoder{//內部私有的類,我特別用了小寫開頭。

 

        static MyObject instance = new MyObject();

 

    }
    private static MyObect obj;
    private Date d = new Data();
    public Data getD(){return this.d;}
    public static MyObect  getInstance(){

 

           return instanceHoder.instance;
    }
}

 

 

 

private static class instanceHoder是類的定義並不會引起初始化。只有在首次調用getInstance時纔會加載

 

instanceHoder類然後初始化instance實例。而靜態初始化是由JVM來保證線程安全的,所以整個過程都不需要同

 

步參與,極大地提高了性能。

轉載自:

http://www.cnblogs.com/programerlrc/articles/5254675.html



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章