說到final你確定知道它是Java中的關鍵字,那麼它所在Java中的做用你知道嗎?不知道的話,請前往這篇瞭解下http://www.javashuo.com/article/p-vewbmfte-eq.htmlhtml
今天咱們來講說final域在JMM中的內存語義。函數
開門見山,對於final域,編譯器和處理器必定要遵照兩個重排序規則(JSR-133才加強了final域):this
1)在構造函數內對一個final域的寫入,與隨後把這個被構造對象的引用賦值給一個引用變量,這個兩個操做不能被重排序。
spa
2)初次讀一個包含final域的對象的引用,與隨後初次讀這個final域,這兩個操做之間不能重排序。線程
下面咱們經過案例來講明這兩點(假設線程1執行writer(),隨後另外一個線程執行reader()方法):code
public class FinalExample { static volatile boolean flag = true; int i = 0; final int j; static FinalExample obj; public FinalExample() { // 構造函數 i = 1; // 寫普通域 j = 2; // 寫final域 } public static void writer() { // 線程1寫入 obj = new FinalExample(); } public static void reader() { // 線程2讀取 FinalExample example = obj; // 讀對象引用 System.out.println(example.i); // 讀普通域 System.out.println(example.j); // 讀final域 } }
寫final域的重排序規則禁止把final域的寫重排序到構造函數以外。這個規則的實現包含下面兩個方面:htm
1)JMM禁止編譯器吧final域的寫重排序到構造函數以外。對象
2)編譯器會在final域的寫以後,構造函數return以前,插入一個StoreStore屏障。這個屏障禁止處理器把final域的寫重排序到構造函數以外。blog
因此線程1執行順序以下圖(其中寫普通域的順序沒法保證,理論上是存在下面三種狀況的,要想驗證普通域是否有重排序的結果有點難,由於沒法保證線程1把普通域重排序後,線程2可以讀取它以前的0值):排序
讀final域的重排序規則是:在一個線程中,初次讀這個對象引用與初次讀該對象包含的final域,JMM禁止處理器重排序這兩個操做。其中編譯器會在讀final域操做的前面插入一個LoadLoad屏障。因爲插入了loadLoad屏障,讀普通域i的操做是不會重排序到讀final域,可是不保證它會重排到讀對象引用這個操做的前面。因此線程2的一個執行順序就能想象到了,這裏就不畫線程2的執行順序圖了。
若是finaly域爲引用類型,JMM中是怎麼處理的呢?對於引用類型,寫final域的重排序規則對編譯器和處理器增長了以下約束:在構造函數內對一個final引用的對象的成員域的寫入,與隨後在構造函數外把這個被構造對象的引用賦值給一個引用變量,這兩個操做之間不能重排序。
以上及其以上都要注意:只針對於構造函數方法內。另外要想以上規則確保,還須要一個條件:在構造函數內部,不能讓這個被構造對象的引用被其餘線程可見,也就是對應引用不能再構造函數中「逸出」。以下案例:
class FinalReferenceEscapeExample { final int i; static FinalReferenceEscapeExample obj; public FinalReferenceEscapeExample() { i = 1; // 1 obj = this; // 2 this引用逸出 } public static void writer() { // 線程1 new FinalReferenceEscapeExample(); } public static void reader() { // 線程2 if (obj != null) { System.out.println(obj.i); } } }
上面程序,第一步寫final域與第二步是不保證重排序的。因此當第一步與第二步重排以後,線程1執行完這步(obj = this)後,時間片分給第二個線程執行,那麼線程2將會獲取final域初始化以前的值,這確定就違背了程序的初衷。