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

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

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() 方法包含三個操作:

  1. 初次讀引用變量 obj;
  2. 初次讀引用變量 obj 指向對象的普通域 j。
  3. 初次讀引用變量 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 final 變量的內存模型

【免費領取資料】java總結思維導圖資料

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

Http 持久連接與 HttpClient 連接池

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

《深入理解Java虛擬機》導圖筆記

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

69 個經典 Spring 面試題和答案

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


分享到:


相關文章: