對於final 域,編譯器和處理器要遵守兩個重排序規則:
- 在構造函數內對一個 final 域的寫入,與隨后把這個被構造對象的引用賦值給一個引用變量,這兩個操作之間不能重排序。
- 初次讀一個包含 final 域的對象的引用,與隨后初次讀這個 final 域,這兩個操作之間不能重排序。
public class FinalExample { int i; //普通變量 final int j; //final變量 static FinalExample obj; public void 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; //讀final域 } }
非標准理解就是:1.對象構造函數內有final域,必須先用構造函數構造對象,再把對象賦給其他引用
2.如果對象有final域,必須先讀對象的引用,再讀final域
寫 final 域的重排序規則可以確保:在引用變量為任意線程可見之前,該引用變量指向的對象的 final 域已經在構造函數中被正確初始化過了。
final引用從構造函數中“溢出”
public class FinalReferenceEscapeExample { final int i; static FinalReferenceEscapeExample obj; public FinalReferenceEscapeExample () { i = 1; //1寫final域 obj = this; //2 this引用在此“逸出” } 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 域正確初始化之后的值。