標籤:lex print 時間片 記憶體模型 cape example 就是 stat adl
一 引言
說到final你肯定知道它是Java中的關鍵字,那麼它所在Java中的作用你知道嗎?不知道的話,請前往這篇瞭解下https://www.cnblogs.com/yuanfy008/p/8021673.html
今天我們來說說final域在JMM中的記憶體語義。
二 final域的重定序
開門見山,對於final域,編譯器和處理器一定要遵守兩個重定序(JSR-133才增強了final域):
1)在建構函式內對一個final域的寫入,與隨後把這個被構造對象的引用賦值給一個引用變數,這個兩個操作不能被重排序。
2)初次讀一個包含final域的對象的引用,與隨後初次讀這個final域,這兩個操作之間不能重排序。
下面我們通過案例來說明這兩點(假設線程1執行writer(),隨後另一個線程執行reader()方法):
public class FinalExample { static volatile boolean flag = true; int i = 0; final int j; static FinalExample obj; public FinalExample() { // 建構函式 i = 1; // 寫普通域 j = 2; // 寫final域 } public static void writer() { // 線程1寫入 obj = new FinalExample(); } public static void reader() { // 線程2讀取 FinalExample example = obj; // 讀對象引用 System.out.println(example.i); // 讀普通域 System.out.println(example.j); // 讀final域 }}
寫final域的重定序禁止把final域的寫重排序到建構函式之外。這個規則的實現包含下面兩個方面:
1)JMM禁止編譯器吧final域的寫重排序到建構函式之外。
2)編譯器會在final域的寫之後,建構函式return之前,插入一個StoreStore屏障。這個屏障禁止處理器把final域的寫重排序到建構函式之外。
所以線程1執行順序如(其中寫普通域的順序無法保證,理論上是存在下面三種情況的,要想驗證普通域是否有重排序的結果有點難,因為無法保證線程1把普通域重排序後,線程2能夠讀取它之前的0值):
讀final域的重定序是:在一個線程中,初次讀這個對象引用與初次讀該對象包含的final域,JMM禁止處理器重排序這兩個操作。其中編譯器會在讀final網域作業的前面插入一個LoadLoad屏障。由於插入了loadLoad屏障,讀普通域i的操作是不會重排序到讀final域,但是不保證它會重排到讀對象引用這個操作的前面。所以線程2的一個執行順序就能想象到了,這裡就不畫線程2的執行順序圖了。
三 final域為參考型別
如果finaly域為參考型別,JMM中是怎麼處理的呢?對於參考型別,寫final域的重定序對編譯器和處理器增加了如下約束:在建構函式內對一個final引用的對象的成員域的寫入,與隨後在建構函式外把這個被構造對象的引用賦值給一個引用變數,這兩個操作之間不能重排序。
以上及其以上都要注意:只針對於建構函式方法內。另外要想以上規則確保,還需要一個條件:在建構函式內部,不能讓這個被構造對象的引用被其他線程可見,也就是對應引用不能再建構函式中“逸出”。如下案例:
class FinalReferenceEscapeExample { final int i; static FinalReferenceEscapeExample obj; public FinalReferenceEscapeExample() { i = 1; // 1 obj = this; // 2 this引用逸出 } public static void writer() { // 線程1 new FinalReferenceEscapeExample(); } public static void reader() { // 線程2 if (obj != null) { System.out.println(obj.i); } }}
上面程式,第一步寫final域與第二步是不保證重排序的。所以當第一步與第二步重排之後,線程1執行完這步(obj = this)後,時間片分給第二個線程執行,那麼線程2將會擷取final域初始化之前的值,這肯定就違背了程式的初衷。
Java記憶體模型-final域的記憶體語義