並發學習筆記09-final域的記憶體語義
- 2020 年 1 月 22 日
- 筆記
該並發學習系列以閱讀《Java並發編程的藝術》一書的筆記為藍本,彙集一些閱讀過程中找到的解惑資料而成。這是一個邊看邊寫的系列,有興趣的也可以先自行購買此書學習。
重排序規則
final域,編譯器和處理器要遵守兩個重排序規則: – 在構造函數內對一個final域的寫入,與隨後把這個被構造對象的引用賦值給一個引用變數,這兩個操作之間不能重排序。 – 初次讀一個包含final域的引用,與隨後初次讀這個final域,這兩個操作之間不能重排序。
寫final域的重排序規則
寫final域的重排序規則禁止把final域的寫重排序到構建函數之外。其實現包含以下2個方面: – JMM禁止編譯器把final域的寫重排序到構建函數之外。 – 編譯器會在final域的寫之後,構造函數return之前插入一個StoreStore屏障。這個屏障禁止處理器把final域的寫重排序到構建函數之外。
寫final域的重排序規則可以確保:在對象引用為任意執行緒可見之前,對象的final域已經被正確初始化過了,而普通域不具有這個保障: 寫普通域的操作可能會被編譯器重排序到構建函數之外,而寫final域的操作被寫final域的重排序規則「限定」在了構造函數之內。
讀final域的重排序規則
讀final域的重排序規則是: – 在一個執行緒中,初次讀對象引用與初次讀該對象包含的final域,JMM禁止處理器重排序這兩個操作(該規則僅針對處理器)。 – 編譯器會在讀final域操作的前面插入一個LoadLoad屏障。
初次讀對象引用與初次讀該對象包含的final域,這兩個操作之間存在間接依賴關係。由於編譯器遵守間接依賴關係,因此編譯器不會重排序這兩個操作。大多數處理器也會遵守間接依賴,也不會重排序這兩個操作。但有少數處理器允許對存在間接依賴關係的操作做重排序(比如alpha處理器),這個規則專門用來針對這種處理器的。
讀final域的重排序規則可以確保:在讀一個對象的final域之前,一定會先讀包含這個final域的對象的引用。如果該引用不為null,則引用對象的final域一定被已經被初始化過了。
final域為引用類型
對於引用類型,寫final域的重排序規則對編譯器和處理器增加了如下約束:在構造函數內對一個final引用的對象的成員域的寫入,與隨後在構造函數外把這個被構造對象的引用賦值給一個引用變數,這兩個操作不能重排序。
final引用不能從構造函數內「溢出」
寫final域的重排序規則可以確保:在對象引用為任意執行緒可見之前,對象的final域已經在構造函數中被正確初始化過了。要到達該效果還需要一個保證: 在構造函數內部,不能讓這個被構造對象的引用為其他執行緒所見,也就是對象引用不能在構造函數中「逸出」。
在構造函數返回之前,被構造對象的引用不能為其他執行緒所見,因為此時的final域可能還沒被初始化。在構造函數返回後,任意執行緒都將保證能看到final域正確初始化之後的值。
final語義在處理器中的實現
以x86處理器為例來了解在處理器中的具體實現。
寫final域的重排序會要求編譯器在final域的寫之後,構造函數return之前插入一個StoreStore屏障。讀final域的重排序規則要求編譯器在讀final域的操作之前插入一個LoadLoad屏障。
- 由於x86處理器不會對寫-寫操作做重排序,所以x86中,寫final域需要的StoreStore屏障會被省略掉。
- 由於x86處理器不會對存在間接依賴關係的操作做重排序,所以x86中,讀final域需要的LoadLoad屏障會被省略掉。
- 綜上,x86中,final域的讀/寫不會插入任何記憶體屏障。
JSR-133增強final的語義
在舊的Java記憶體模型中,一個最嚴重的缺陷就是執行緒可能看到final域的值會改變。比如,一個執行緒當前看到一個整形final域的值為0(還未初始化之前的默認值),過了一段時間之後這個執行緒再去讀這個final域的值時,發現變成了1(被某個執行緒初始化之後的值)。最常見的例子就是在舊的Java記憶體模型中,String的值可能會改變。
為了修復這個漏洞,JSR-133專家組增強了final的語義。通過為final域增加寫和讀的重排序規則,可以確為Java程式設計師提供初始化安全保證:只要對象是正確構造的(被構造對象的引用在構造函數中沒有「逸出」),那麼不需要使用同步(指lock和volatile的使用)就可以保證任意執行緒都可以看到這個final域在構造函數中被初始化之後的值。