Java基礎(chǔ)4:final關(guān)鍵字

一、final

使用final關(guān)鍵字做標(biāo)識有“最終的”含義。

  1. final 修飾類,則該類不允許被繼承。
  2. final 修飾方法,則該方法不允許被重寫。
  3. final 修飾屬性,則該類的該屬性不會進行隱式的初始化,所以 該final 屬性的初始化屬性必須有值,或在構(gòu)造方法中賦值(但只能選其一,且必須選其一)。
  4. final修飾的變量稱為常量(大寫字母表示),只能被賦值一次,且賦值之后無法改變,這里的變量又可以分為基本類型變量和引用類型變量,final修飾基本類型變量時,變量的值不可改變;修飾引用變量時,變量指向的對象地址不可改變,但對象內(nèi)部的屬性可以發(fā)生改變。
@Test
public void test() {
    final int i = 1;
    i = 2;//不能重新賦值

    final List<Integer> list = new ArrayList<>();
    list.add(1);//引用對象可以改變內(nèi)部屬性
    list.add(2);
    list.add(3);
    list = new LinkedList<>();//不能重新賦值
}

基本類型和對象引用地址存放在棧內(nèi)存中,對象實例存放在堆內(nèi)存中。從JVM角度來看,對于final變量,不能改變內(nèi)存中 棧 里面的值,但不影響操作堆里對象的屬性。

二、重排序

對于 final 域,編譯器和處理器要遵守兩個重排序規(guī)則:

  1. 在構(gòu)造函數(shù)內(nèi)對一個 final 域的寫入,與隨后把這個被構(gòu)造對象的引用賦值給一個引用變量,這兩個操作之間不能重排序。
  2. 初次讀一個包含 final 域的對象的引用,與隨后初次讀這個 final 域,這兩個操作之間不能重排序。
public class FinalExample {
    int i;                            // 普通變量 
    final int j;                      //final 變量 
    static FinalExample obj;

    public void FinalExample () {     // 構(gòu)造函數(shù) 
        i = 1;                        // 寫普通域 
        j = 2;                        // 寫 final 域 
    }

    public static void writer () {    // 寫線程 A 執(zhí)行 
        obj = new FinalExample ();
    }

    public static void reader () {       // 讀線程 B 執(zhí)行 
        FinalExample object = obj;       // 讀對象引用 
        int a = object.i;                // 讀普通域 
        int b = object.j;                // 讀 final 域 
    }
}

這里假設(shè)一個線程 A 執(zhí)行 writer () 方法,隨后另一個線程 B 執(zhí)行 reader () 方法。

下面我們通過這兩個線程的交互來說明這兩個規(guī)則。

1)寫 final 域的重排序規(guī)則

寫 final 域的重排序規(guī)則禁止把 final 域的寫重排序到構(gòu)造函數(shù)之外。這個規(guī)則的實現(xiàn)包含下面 2 個方面:

  • JMM 禁止編譯器把 final 域的寫重排序到構(gòu)造函數(shù)之外。
  • 編譯器會在 final 域的寫之后,構(gòu)造函數(shù) return 之前,插入一個 StoreStore 屏障。這個屏障禁止處理器把 final 域的寫重排序到構(gòu)造函數(shù)之外。

下面是一種可能的執(zhí)行時序:


image.png

在上圖中,寫普通域的操作被編譯器重排序到了構(gòu)造函數(shù)之外,讀線程 B 錯誤的讀取了普通變量 i 初始化之前的值。而寫 final 域的操作,被寫 final 域的重排序規(guī)則“限定”在了構(gòu)造函數(shù)之內(nèi),讀線程 B 正確的讀取了 final 變量初始化之后的值。

寫 final 域的重排序規(guī)則可以確保:在對象引用為任意線程可見之前,對象的 final 域已經(jīng)被正確初始化過了,而普通域不具有這個保障。以上圖為例,在讀線程 B“看到”對象引用 obj 時,很可能 obj 對象還沒有構(gòu)造完成(對普通域 i 的寫操作被重排序到構(gòu)造函數(shù)外,此時初始值 2 還沒有寫入普通域 i)。

2)讀 final 域的重排序規(guī)則

在一個線程中,初次讀對象引用與初次讀該對象包含的 final 域,JMM 禁止處理器重排序這兩個操作(注意,這個規(guī)則僅僅針對處理器)。編譯器會在讀 final 域操作的前面插入一個 LoadLoad 屏障。

下面是一種可能的執(zhí)行時序:


image.png

在上圖中,讀對象的普通域的操作被處理器重排序到讀對象引用之前。讀普通域時,該域還沒有被寫線程 A 寫入,這是一個錯誤的讀取操作。而讀 final 域的重排序規(guī)則會把讀對象 final 域的操作“限定”在讀對象引用之后,此時該 final 域已經(jīng)被 A 線程初始化過了,這是一個正確的讀取操作。

讀 final 域的重排序規(guī)則可以確保:在讀一個對象的 final 域之前,一定會先讀包含這個 final 域的對象的引用。在這個示例程序中,如果該引用不為 null,那么引用對象的 final 域一定已經(jīng)被 A 線程初始化過了。

總結(jié):寫 final 域的重排序規(guī)則會要求譯編器在 final 域的寫之后,構(gòu)造函數(shù) return 之前,插入一個 StoreStore 障屏。讀 final 域的重排序規(guī)則要求編譯器在讀 final 域的操作前面插入一個 LoadLoad 屏障。

3)JSR-133 為什么要增強 final 的語義

在舊的 Java 內(nèi)存模型中 ,最嚴(yán)重的一個缺陷就是線程可能看到 final 域的值會改變。比如,一個線程當(dāng)前看到一個整形 final 域的值為 0(還未初始化之前的默認值),過一段時間之后這個線程再去讀這個 final 域的值時,卻發(fā)現(xiàn)值變?yōu)榱?1(被某個線程初始化之后的值)。最常見的例子就是在舊的 Java 內(nèi)存模型中,String 的值可能會改變

為了修補這個漏洞,JSR-133 專家組增強了 final 的語義。通過為 final 域增加寫和讀重排序規(guī)則,可以為 java 程序員提供初始化安全保證:只要對象是正確構(gòu)造的(被構(gòu)造對象的引用在構(gòu)造函數(shù)中沒有“逸出”),那么不需要使用同步(指 lock 和 volatile 的使用),就可以保證任意線程都能看到這個 final 域在構(gòu)造函數(shù)中被初始化之后的值。

final 只能保證初始化完成后的可見性,無法禁止指令重排序,這點和 volatile 關(guān)鍵字是有區(qū)別的。

參考鏈接:
http://www.itdecent.cn/p/b4d4506d3585
https://www.infoq.cn/article/java-memory-model-6
http://www.itdecent.cn/p/067b6c89875a

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容