java程序媛之家
對於 final 域,編譯器和處理器要遵守兩個重排序規則:
- 在構造函數內對一個 final 域的寫,與隨後把這個構造對象的引用賦值給一個變量,這兩個操作之間不能重排序
- 初次讀一個包含 final 域的對象的引用,與隨後初次讀這個 final 域,這兩個操作之間不能重排序
舉個例子:
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
寫 final 域的重排序規則可以確保:在對象引用為任意線程可見之前,對象的 final 域已經被正確初始化過了,而普通域不具有這個保障。
讀 final 域的重排序規則
讀 final 域的重排序規則如下:
在一個線程中,初次讀對象引用與初次讀該對象包含的 final 域,JMM 禁止處理器重排序這兩個操作(注意,這個規則僅僅針對處理器)。編譯器會在讀 final 域操作的前面插入一個 LoadLoad 屏障。
reader() 方法包含三個操作:
- 初次讀引用變量 obj;
- 初次讀引用變量 obj 指向對象的普通域 j。
- 初次讀引用變量 obj 指向對象的 final 域 i。
讀 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內存模型
--(完) --
看完本文有收穫?請轉發分享給更多人
關注「java程序媛之家」,提升Java技能
【免費領取資料】java總結思維導圖資料
Http 持久連接與 HttpClient 連接池
《深入理解Java虛擬機》導圖筆記
69 個經典 Spring 面試題和答案
閱讀更多 java程序媛之家 的文章