深入理解 Java final 變量的內存模型
原文出處: 任春曉
對于 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 與讀成員域之間沒有重排序。那么執行時序可能如下:
寫 final 域的重排序規則可以確保:在對象引用為任意線程可見之前,對象的 final 域已經被正確初始化過了,而普通域不具有這個保障。
讀 final 域的重排序規則
讀 final 域的重排序規則如下:
- 在一個線程中,初次讀對象引用與初次讀該對象包含的 final 域,JMM 禁止處理器重排序這兩個操作(注意,這個規則僅僅針對處理器)。編譯器會在讀 final 域操作的前面插入一個 LoadLoad 屏障。
reader() 方法包含三個操作:
- 初次讀引用變量 obj;
- 初次讀引用變量 obj 指向對象的普通域 j。
- 初次讀引用變量 obj 指向對象的 final 域 i。
現在我們假設寫線程 A 沒有發生任何重排序,那么執行時序可能是:
上面的圖可以看到對普通變量 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內存模型