Java 中 final 內存語義

        對於final域,編譯器和理器要遵照兩個重排序規則
                1)在構造函數內對一個final域的寫入,與隨後把個被構造象的引用賦值給一個引用量,兩個操做之不能重排序。
                2)初次讀一個包含final域的象的引用,與隨後初次讀這final域,兩個操做之不能重排序。
        下面經過一些示例性的代來分別說兩個規則 編程

        寫final域的重排序規則禁止把final域的寫重排序到構造函數以外。規則實現包含下面2個方面。
                1)JMM禁止編譯器把final域的寫重排序到構造函數以外。
                2)編譯器會在final域的寫以後,構造函數return以前,插入一個StoreStore屏障。個屏障禁止理器把final域的寫重排序到構造函數以外。
        如今讓分析writer()方法。writer()方法只包含一行代finalExample=new FinalExample()行代包含兩個步,以下。
                1)構造一個FinalExample類型的象。
                2)把這個對象的引用賦值給引用obj
        假設線程B讀對象引用與讀對象的成域之沒有重排序(上會什麼須要個假),3-29是一種可能的序。在下中,寫普通域的操做被編譯器重排序到了構造函數以外,讀線B錯誤取了普通i初始化以前的。而寫final域的操做,被寫final域的重排序規則限定在了構造函數以內,讀線B正確地取了final量初始化以後的
        寫final域的重排序規則能夠確保:在象引用任意程可以前,象的final域已被正確初始化了,而普通域不具備個保障。以上圖爲例,在讀線B「看到象引用obj,極可能obj沒有構造完成(普通域i的寫操做被重排序到構造函數外,此初始1沒有寫入普通域i)。 安全

        讀final域的重排序規則是,在一個程中,初次讀對象引用與初次讀該對象包含的final域,JMM禁止理器重排序兩個操做(注意,規則僅僅針對處理器)。編譯器會在final域操做的前面插入一個LoadLoad屏障。
        初次讀對象引用與初次讀該對象包含的final域,兩個操做之存在接依關係。因爲編譯器遵照接依關係,所以編譯器不會重排序兩個操做。大多數理器也會遵照接依,也不會重排序兩個操做。但有少數理器允許對存在接依關係的操做作重排序(好比alpha理器),規則就是專門用來針對這理器的。
        reader()方法包含3個操做。
            ·初次讀引用obj
            ·初次讀引用obj指向象的普通域j
            ·初次讀引用obj指向象的finali
        如今假設A沒有生任何重排序,同程序在不遵照接依理器上行,下所示是一種可能的序。 併發

        在上圖中,讀對象的普通域的操做被理器重排序到讀對象引用以前。普通域沒有被寫A寫入,是一個錯誤取操做。而final域的重排序規則會把讀對final域的操做限定讀對象引用以後,此時該final域已A程初始化了,是一個正確的取操做。
        讀final域的重排序規則能夠確保:在一個象的final域以前,必定會先包含final域的象的引用。在個示例程序中,若是引用不null,那麼引用象的final域必定已A程初始化了。 函數

-------------------------------------------------------------------------------spa

        上面咱們看到的final域是基數據型,若是final域是引用型,將會有什麼效果?看下列示例代 線程

        本例final域一個引用型,它引用一個int型的數組對象。於引用型,寫final域的重排序規則對編譯器和理器增長了以下束:在構造函數內一個final引用的象的成域的寫入,與隨後在構造函數外把個被構造象的引用賦值給一個引用量,兩個操做之不能重排序。
        對上面的示例程序,假設首先AwriterOne()方法,行完後BwriterTwo()方法,行完後Creader()方法。下圖中是一種可能的序。
        在圖中,1final域的寫入,2對這final域引用的象的成域的寫入,3是把被構造的象的引用賦值給某個引用量。裏除了前面提到的1不能和3重排序外,23也不能重排序。
        JMM能夠確保讀線C至少能看到寫A在構造函數中final引用象的成域的寫入。即C至少能看到數0值爲1。而寫B元素的寫入,讀線C可能看獲得,也可能看不到。JMM不保證線B的寫入對讀線C,因B讀線C存在數據爭,此果不可知。對象

        若是想要確保讀線程C看到寫B元素的寫入,寫B讀線C須要使用同步原lockvolatile)來確保內存可性。 blog

        前面咱們提到,寫final域的重排序規則能夠確保:在引用任意程可以前,引用量指向的象的final域已在構造函數中被正確初始化了。其,要獲得個效果,須要一個保:在構造函數內部,不能讓這個被構造象的引用其餘程所,也就是象引用不能在構造函數中逸出問題來看下面的示例代排序

        假設一個Awriter()方法,另外一個Breader()方法。裏的操做2使得未完成構造前就爲線B。即便裏的操做2是構造函數的最後一步,且在程序中操做2排在操做1後面,read()方法的程仍然可能沒法看到final域被初始化後的,因爲這裏的操做1和操做2可能被重排序。實際序可能如所示。內存

        從圖中能夠看出:在構造函數返回前,被構造象的引用不能其餘程所,因final域可能沒有被初始化。在構造函數返回後,任意程都將保能看到final域正確初始化以後的

JSR-133什麼要增final語義

        在舊的Java內存模型中,一個最嚴重的缺陷就是程可能看到final域的會改。好比,一個程當前看到一個整型final域的值爲0未初始化以前的默認值),一段時間以後程再去讀這final域的值時,卻發現值變爲1(被某個程初始化以後的)。最常的例子就是在舊的Java內存模型中,String可能會改了修補這個漏洞,JSR-133final語義。通過爲final域增長寫和重排序規則,能夠Java程序提供初始化安全保:只要象是正確構造的(被構造象的引用在構造函數中沒有逸出),那麼不須要使用同步(指lockvolatile的使用)就能夠保任意程都能看到final域在構造函數中被初始化以後的 

參考:

《Java 併發編程的藝術》

相關文章
相關標籤/搜索