深入理解final關(guān)鍵字

final關(guān)鍵字特性

final關(guān)鍵字在java中使用非常廣泛,可以申明成員變量、方法、類(lèi)、本地變量。一旦將引用聲明為final,將無(wú)法再改變這個(gè)引用。final關(guān)鍵字還能保證內(nèi)存同步,本博客將會(huì)從final關(guān)鍵字的特性到從java內(nèi)存層面保證同步講解。這個(gè)內(nèi)容在面試中也有可能會(huì)出現(xiàn)。

final使用

final變量

final變量有成員變量或者是本地變量(方法內(nèi)的局部變量),在類(lèi)成員中final經(jīng)常和static一起使用,作為類(lèi)常量使用。其中類(lèi)常量必須在聲明時(shí)初始化,final成員常量可以在構(gòu)造函數(shù)初始化。

public class Main {
    public static final int i; //報(bào)錯(cuò),必須初始化 因?yàn)槌A吭诔A砍刂芯痛嬖诹?,調(diào)用時(shí)不需要類(lèi)的初始化,所以必須在聲明時(shí)初始化
    public static final int j;
    Main() {
        i = 2;
        j = 3;
    }
}

就如上所說(shuō)的,對(duì)于類(lèi)常量,JVM會(huì)緩存在常量池中,在讀取該變量時(shí)不會(huì)加載這個(gè)類(lèi)。


public class Main {
    public static final int i = 2;
    Main() {
        System.out.println("調(diào)用構(gòu)造函數(shù)"); // 該方法不會(huì)調(diào)用
    }
    public static void main(String[] args) {
        System.out.println(Main.i);
    }
}

final方法

final方法表示該方法不能被子類(lèi)的方法重寫(xiě),將方法聲明為final,在編譯的時(shí)候就已經(jīng)靜態(tài)綁定了,不需要在運(yùn)行時(shí)動(dòng)態(tài)綁定。final方法調(diào)用時(shí)使用的是invokespecial指令。

class PersonalLoan{
    public final String getName(){
        return"personal loan”;
    }
}
 
class CheapPersonalLoan extends PersonalLoan{
    @Override
    public final String getName(){
        return"cheap personal loan";//編譯錯(cuò)誤,無(wú)法被重載
    }
    
    public String test() {
        return getName(); //可以調(diào)用,因?yàn)槭莗ublic方法
    }
}

final類(lèi)

final類(lèi)不能被繼承,final類(lèi)中的方法默認(rèn)也會(huì)是final類(lèi)型的,java中的String類(lèi)和Integer類(lèi)都是final類(lèi)型的。

final class PersonalLoan{}
 
class CheapPersonalLoan extends PersonalLoan {  //編譯錯(cuò)誤,無(wú)法被繼承 
}

final關(guān)鍵字的知識(shí)點(diǎn)

  1. final成員變量必須在聲明的時(shí)候初始化或者在構(gòu)造器中初始化,否則就會(huì)報(bào)編譯錯(cuò)誤。final變量一旦被初始化后不能再次賦值。
  2. 本地變量必須在聲明時(shí)賦值。 因?yàn)闆](méi)有初始化的過(guò)程
  3. 在匿名類(lèi)中所有變量都必須是final變量。
  4. final方法不能被重寫(xiě), final類(lèi)不能被繼承
  5. 接口中聲明的所有變量本身是final的。類(lèi)似于匿名類(lèi)
  6. final和abstract這兩個(gè)關(guān)鍵字是反相關(guān)的,final類(lèi)就不可能是abstract的。
  7. final方法在編譯階段綁定,稱(chēng)為靜態(tài)綁定(static binding)。
  8. 將類(lèi)、方法、變量聲明為final能夠提高性能,這樣JVM就有機(jī)會(huì)進(jìn)行估計(jì),然后優(yōu)化。

final方法的好處:

  1. 提高了性能,JVM在常量池中會(huì)緩存final變量
  2. final變量在多線程中并發(fā)安全,無(wú)需額外的同步開(kāi)銷(xiāo)
  3. final方法是靜態(tài)編譯的,提高了調(diào)用速度
  4. final類(lèi)創(chuàng)建的對(duì)象是只可讀的,在多線程可以安全共享

從java內(nèi)存模型中理解final關(guān)鍵字

java內(nèi)存模型對(duì)final域遵守如下兩個(gè)重拍序規(guī)則

  1. 初次讀一個(gè)包含final域的對(duì)象的引用和隨后初次寫(xiě)這個(gè)final域,不能重拍序。
  2. 在構(gòu)造函數(shù)內(nèi)對(duì)final域?qū)懭耄S后將構(gòu)造函數(shù)的引用賦值給一個(gè)引用變量,操作不能重排序。

以上兩個(gè)規(guī)則就限制了final域的初始化必須在構(gòu)造函數(shù)內(nèi),不能重拍序到構(gòu)造函數(shù)之外,普通變量可以。

具體的操作是

  1. java內(nèi)存模型在final域?qū)懭牒蜆?gòu)造函數(shù)返回之前,插入一個(gè)StoreStore內(nèi)存屏障,靜止處理器將final域重拍序到構(gòu)造函數(shù)之外。
  2. java內(nèi)存模型在初次讀final域的對(duì)象和讀對(duì)象內(nèi)final域之間插入一個(gè)LoadLoad內(nèi)存屏障。

new一個(gè)對(duì)象至少有以下3個(gè)步驟

  1. 在堆中申請(qǐng)一塊內(nèi)存空間
  2. 對(duì)象進(jìn)行初始化
  3. 將內(nèi)存空間的引用賦值給一個(gè)引用變量,可以理解為調(diào)用invokespecial指令

普通成員變量在初始化時(shí)可以重排序?yàn)?-3-2,即被重拍序到構(gòu)造函數(shù)之外去了。 final變量在初始化必須為1-2-3。

讀寫(xiě)final域重拍序規(guī)則

public class FinalExample {
    int i;               
    final int j;
    static FinalExample obj;

    public void FinalExample () {
        i = 1;                   // 1
        j = 2;                   // 2
    }

    public static void writer () {  //寫(xiě)線程A  
        obj = new FinalExample ();  // 3
    }

    public static void reader () {       //讀線程B執(zhí)行
        if(obj != null) {               //4
            int a = object.i;           //5
            int b = object.j;           //6
        }
    }
}

我們可以用happens-before來(lái)分析可見(jiàn)性。結(jié)果是保證a讀取到的值可能為0,或者1 而b讀取的值一定為2。
首先,由final的重拍序規(guī)則決定3HB2,但是3和1不存在HB關(guān)系,原因在上面說(shuō)過(guò)了。 因?yàn)榫€程B在線程A之后執(zhí)行,所以3HB4。
那么2和4的HB關(guān)系怎么確定?? final的重拍序規(guī)則規(guī)定final的賦值必須在構(gòu)造函數(shù)的return之前。所以2HB4。因?yàn)樵谝粋€(gè)線程內(nèi)4HB6.所以可以得出結(jié)論2HB5。則b一定能得到j(luò)的最新值。而a就不一定了,因?yàn)闆](méi)有HB關(guān)系,可以讀到任意值。

HB判斷可見(jiàn)性關(guān)系真是太方便了??梢詤⒖嘉业牧硗庖粋€(gè)博客http://medesqure.top/2018/08/25/happen-before/

可能發(fā)生的執(zhí)行時(shí)序如下所示。


image

final對(duì)象是引用類(lèi)型

如果final域是一個(gè)引用類(lèi)型,比如引用的是一個(gè)int類(lèi)型的數(shù)組。對(duì)于引用類(lèi)型,寫(xiě)final域的重拍序規(guī)則增加了如下的約束

  1. 在構(gòu)造函數(shù)內(nèi)對(duì)一個(gè)final引用的對(duì)象的成員域的寫(xiě)入和隨后在構(gòu)造函數(shù)外將被構(gòu)造對(duì)象的引用賦值給引用變量之間不能重拍序。 即先寫(xiě)int[]數(shù)組的內(nèi)容,再將引用拋出去。
public class FinalReferenceExample {
    final int[] intArray;                     //final是引用類(lèi)型
    static FinalReferenceExample obj;
    
    public FinalReferenceExample () {        //構(gòu)造函數(shù)  在構(gòu)造函數(shù)中不能被重排序 final類(lèi)型在聲明或者在構(gòu)造函數(shù)中要賦值。
        intArray = new int[1];              //1
        intArray[0] = 1;                   //2
    }
    
    public static void writerOne () {          //寫(xiě)線程A執(zhí)行
        obj = new FinalReferenceExample ();  //3
    }
    
    public static void writerTwo () {          //寫(xiě)線程B執(zhí)行
        obj.intArray[0] = 2;                 //4
    }
    
    public static void reader () {              //讀線程C執(zhí)行
        if (obj != null) {                    //5
            int temp1 = obj.intArray[0];       //6
        }
    }
}

JMM保證了3和2之間的有序性。同樣可以使用HB原則去分析,這里就不分析了。執(zhí)行順序如下所示。


6DBA7734-EFF8-4AC2-8E3B-E1645889A109

final引用不能從構(gòu)造函數(shù)“逸出”

JMM對(duì)final域的重拍序規(guī)則保證了能安全讀取final域時(shí)已經(jīng)在構(gòu)造函數(shù)中被正確的初始化了。
但是如果在構(gòu)造函數(shù)內(nèi)將被構(gòu)造函數(shù)的引用為其他線程可見(jiàn),那么久存在對(duì)象引用在構(gòu)造函數(shù)中逸出,final的可見(jiàn)性就不能保證。 其實(shí)理解起來(lái)很簡(jiǎn)單,就是在其他線程的角度去觀察另一個(gè)線程的指令其實(shí)是重拍序的。

public class FinalReferenceEscapeExample {
    final int i;
    static FinalReferenceEscapeExample obj;
    
    public FinalReferenceEscapeExample () {
        i = 1;       //1寫(xiě)final域
        obj = this;  //2 this引用在此“逸出”  因?yàn)閛bj不是final類(lèi)型的,所以不用遵守可見(jiàn)性  }
    
    public static void writer() {
        new FinalReferenceEscapeExample ();
    }

    public static void reader {
        if (obj != null) {                     //3
            int temp = obj.i;                 //4
        }
    }
}

操作1的和操作2可能被重拍序。在其他線程觀察時(shí)就會(huì)訪問(wèn)到未被初始化的變量i,可能的執(zhí)行順序如圖所示。


AAF34760-7112-463C-852F-25CB775AFD62

本文結(jié)束,歡迎閱讀。
本人博客 http://medesqure.top/ 歡迎觀看

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

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

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