Java 內存模型(JMM)對 final 域有特殊的重排序規則,提供初始化安全保證。本文通過代碼示例、時序圖解析寫/讀 final 域的兩大重排序規則,並深入探討引用類型 final 域、構造函數逸出問題及 x86 處理器實現細節。
| 規則 | 約束對象 | 實現手段 | 保證效果 |
|---|---|---|---|
| 寫 final 域重排序規則 | 編譯器、處理器 | 禁止 final 域寫重排到構造函數外 + 構造函數返回前插入 StoreStore 屏障 | 對象引用可見時,final 域已正確初始化 |
| 讀 final 域重排序規則 | 處理器 | 禁止初次讀對象引用與讀 final 域重排序 + 讀 final 域前插入 LoadLoad 屏障 | 讀 final 域前,必先讀對象引用,保證看到初始化值 |
public class FinalExample {
int i; // 普通域
final int j; // final 域
static FinalExample obj;
public FinalExample() {
i = 1; // 寫普通域
j = 2; // 寫 final 域
}
public static void writer() { obj = new FinalExample(); } // 線程 A
public static void reader() { // 線程 B
FinalExample object = obj;
int a = object.i; // 可能讀到 0(重排序導致)
int b = object.j; // 一定讀到 2(受 final 規則保護)
}
}
普通域 i 的寫操作可能被重排序到構造函數外,導致線程 B 讀到初始值 0。而 final 域 j 的寫被「限定」在構造函數內,配合 StoreStore 屏障,確保線程 B 看到對象引用時 j 已初始化為 2。
處理器可能將讀普通域操作重排到讀對象引用之前,導致讀取未初始化的值。讀 final 域規則通過 LoadLoad 屏障強制讀 final 域操作在讀對象引用之後,此時 final 域已被寫線程初始化。
public class FinalReferenceExample {
final int[] intArray; // final 引用類型
static FinalReferenceExample obj;
public FinalReferenceExample() {
intArray = new int[1]; // 1: 寫 final 引用
intArray[0] = 1; // 2: 寫引用對象的成員域
}
public static void writerOne() { obj = new FinalReferenceExample(); } // 3: 寫對象引用
public static void writerTwo() { obj.intArray[0] = 2; } // 4: 另一線程修改數組元素
public static void reader() { // 線程 C
if (obj != null) {
int temp1 = obj.intArray[0]; // 至少看到 1,不保證看到 2
}
}
}
JMM 確保:1 不能與 3 重排序,2 也不能與 3 重排序。讀線程 C 至少能看到構造函數中對 final 引用對象成員域的寫入(即數組下標 0 為 1)。但寫線程 B 對數組元素的修改(操作 4)對 C 不保證可見,存在數據競爭,需額外同步手段。
public class FinalReferenceEscapeExample {
final int i;
static FinalReferenceEscapeExample obj;
public FinalReferenceEscapeExample() {
i = 1; // 1: 寫 final 域
obj = this; // 2: this 引用逸出(構造未完成就對其他線程可見)
}
}
即使操作 2 在代碼順序上在操作 1 之後,編譯器/處理器仍可能重排序。線程 B 可能看到 obj != null 但 obj.i 仍為 0。結論:構造函數返回前,被構造對象引用不可為其他線程可見。
舊內存模型中,線程可能先看到 final 域默認值(如 0),後看到初始化值(如 1),甚至 String 值可能改變。JSR-133 通過最終域重排序規則,提供初始化安全保證:正確構造的對象(無 this 逸出)無需同步,任意線程均可見 final 域初始化後的值。