併發編程系列之Final域的內存語義

前言

上節我們講了鎖的內存語義,在同步原語中我們已經講了兩個,今天再來介紹另一個同步原語Final域,瞭解下final域的內存語義以及重排序規則在處理器中又是如何實現的,並結合前面的volatile和鎖,大家可以進行對比下,OK,開始我們今天的併發之旅吧。

final域的重排序規則

對於final域,編譯器和處理器需要遵循兩個重排序規則:

  • 對一個構造函數內final域的寫入,與後續把這個構造對象的引用賦值給一個引用變量,這2個操作之間是不能重排序的,相當於對一個final域的寫和讀不能重排序;

  • 對一個final域對象的引用第一次讀,和後續初次讀這個final域本身,這2個操作之間不能重排序,相當於第一次讀final域引用和final域不能重排序;

final域寫的重排序規則

禁止把final域的寫重排序到構造函數之外,這個規則包含下面2個方面的實現:

  • JMM禁止編譯器把final域的寫重排序到構造函數之外;

  • 編譯器會在final域的寫入之後,構造函數return之前,插入一個StoreStore屏障。這個屏障禁止處理器把final域的寫重排序到構造函數之外;

  • 此外,如果final域本身爲引用類型時情況會有所不同,當final域爲引用類型時,寫final域重排序規則增加了一個實現:在構造函數內對一個final引用的對象的成員域的寫入,與隨後在構造函數外把這個被構造對象的引用賦值給一個引用變量,這兩個操作之間不能重排序;

final域寫重排序規則可以保證:在對象引用被任意線程可見之前,對象final域已經被正確的初始化結束,也就是說,任意一個線程在讀final域對象引用之前,這個對象一定是初始化完畢的狀態(其實還需要一點保證,對象引用不能在構造函數中“逸出”,如逸出了,很有可能發生還未被初始化結束的對象引用被賦值給其他變量),而普通域對象則不一定能保證。

final域讀的重排序規則

在一個線程中,初次讀對象的引用與初次讀這個對象本身的final域(間接依賴),JMM禁止重排序這兩個操作,編譯器會在讀final域的操作前面加一個LoadLoad屏障

final域讀重排序規則可以保證:在讀一個對象的final域之前,一定先讀包含這個final域的對象的引用,如果一個final對象的引用不爲null,那麼該final域一定已經被初始化。

final域內存語義在處理器中的實現

上面提到,寫final域的重排序規則要求編譯器在final域的寫之後,構造函數return之前插入一個StoreStore屏障,讀final域重排序規則要求編譯器在讀final域的操作前面插入一個LoadLoad屏障;

但是由於處理器(以X86爲例)不會對寫-寫和存在間接依賴關係的操作做重排序,所以這兩種操作的屏障在處理器中都會被省略掉。

JSR-133對final語義的增強

舊的內存模型中,有個很嚴重的缺陷就是線程可能讀到的final域的值會改變,就是說線程可能先看到一個final域對象的未初始值的默認值爲0,然後後面讀取到的是初始化之後的值1,爲了避免這個問題,JSR-133對final語義做了如下的增強:

 

通過爲final域增加寫和讀重排序規則,可以爲Java程序員提供初始化安全保證:只要對象是正確構造的(被構造對象的引用在構造函數中沒有“逸出”),那麼不需要使用同步(指lock和volatile的使用)就可以保證任意線程都能看到這個final域在構造函數中被初始化之後的值。

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