深入理解 Java final 變量的內存模型

jopen 9年前發布 | 9K 次閱讀 Java Java開發

原文出處: 任春曉

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

  • 在構造函數內對一個 final 域的寫,與隨后把這個構造對象的引用賦值給一個變量,這兩個操作之間不能重排序
  • 初次讀一個包含 final 域的對象的引用,與隨后初次讀這個 final 域,這兩個操作之間不能重排序
  • </ul>

    舉個例子:

    public class FinalExample {
        int i;// 普通變量
        final int j;// final 變量
        static FinalExample obj;
    
        public FinalExample() {
            i = 1;// 寫普通域
            j = 2;// 寫 final 域
        }
    
        public static void writer() {// 寫線程 A 執行
            obj = new FinalExample();
        }
    
        public static void reader() {// 讀線程 B 執行
            FinalExample object = obj;
            int a = object.i;
            int b = object.j;
        }
    }

    這里假設一個線程 A 執行 writer ()方法,隨后另一個線程 B 執行 reader ()方法。

    寫 final 域的重排序規則

    在寫 final 域的時候有兩個規則:

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

    分析上面的代碼。

    write 方法,只包含一行obj = new FinalExample();,但是包含兩個步驟:

    • 構造一個 FinalExample 對象
    • 把對象的引用賦值給 obj

    假設線程 B 當中讀 obj 與讀成員域之間沒有重排序。那么執行時序可能如下:

     深入理解 Java final 變量的內存模型

    寫 final 域的重排序規則可以確保:在對象引用為任意線程可見之前,對象的 final 域已經被正確初始化過了,而普通域不具有這個保障。

    讀 final 域的重排序規則

    讀 final 域的重排序規則如下:

    • 在一個線程中,初次讀對象引用與初次讀該對象包含的 final 域,JMM 禁止處理器重排序這兩個操作(注意,這個規則僅僅針對處理器)。編譯器會在讀 final 域操作的前面插入一個 LoadLoad 屏障。

    reader() 方法包含三個操作:

    1. 初次讀引用變量 obj;
    2. 初次讀引用變量 obj 指向對象的普通域 j。
    3. 初次讀引用變量 obj 指向對象的 final 域 i。

    現在我們假設寫線程 A 沒有發生任何重排序,那么執行時序可能是:

     深入理解 Java final 變量的內存模型

    上面的圖可以看到對普通變量 i 的讀取重排序到了讀對象引用之前,在讀普通域時候,該域還沒被寫線程 A 寫入,這是一個錯誤的讀取操作。而讀 final 域已經被 A 線程初始化了,這個讀取操作是正確的。

    讀 final 域的重排序規則可以確保:在讀一個對象的 final 域之前,一定會先讀包含 這個 final 域的對象的引用。在這個示例程序中,如果該引用不為 null,那么引用 對象的 final 域一定已經被 A 線程初始化過了。

    如果 final 域是引用類型

    如果 final 域是引用類型,寫 final 域的重排序規則對編譯器和處理器增加了如下約束:

    • 在構造函數內對一個 final 引用的對象的成員域的寫入,與隨后在構造函數外把這個被構造對象的引用賦值給一個引用變量,這兩個操作之間不能重排序。

    如下代碼例子:

    public class FinalReferenceExample {
        final int[] intArray;
        static FinalReferenceExample obj;
    
        public FinalReferenceExample() {
            intArray = new int[1];// 1
            intArray[0] = 1;// 2
        }
    
        public static void writerOne() {// A線程執行
            obj = new FinalReferenceExample(); // 3
        }
    
        public static void reader() {// 寫線程 B 執行
            if (obj != null) { // 4
                int temp1 = obj.intArray[0]; // 5
            }
        }
    }

    假設首先線程 A 執行 writerOne()方法,執行完后線程 B 執行reader 方法,JMM 可以確保讀線程 B 至少能看到寫線程 A 在構造函數中對 final 引用對象的成員域的寫入。

    避免對象引用在構造函數當中溢出

    代碼如下:

    public class FinalReferenceEscapeExample {
        final int i;
        static FinalReferenceEscapeExample obj;
    
        public FinalReferenceEscapeExample() {
            i = 1;// 1
            obj = this;// 2 避免怎么做!!!
        }
    
        public static void writer() {
            new FinalReferenceEscapeExample();
        }
    
        public static void reader() {
            if (obj != null) {// 3
                int temp = obj.i; // 4
            }
        }
    }

    假設一個線程 A 執行 writer()方法,另一個線程 B 執行 reader()方法。

    這里的操作 2 使得對象還未完成構造前就為線程 B 可見。即使這里的操作 2 是構造函數的最后 一步,且即使在程序中操作 2 排在操作 1 后面,執行 read()方法的線程仍然可能無 法看到 final 域被初始化后的值,因為這里的操作 1 和操作 2 之間可能被重排序。

    在構造函數返回前,被構造對象的引用不能為其他線程可 見,因為此時的 final 域可能還沒有被初始化。在構造函數返回后,任意線程都將 保證能看到 final 域正確初始化之后的值。

    【參考資料】

    深入理解java內存模型

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!