你以為你真的了解final嗎击喂?

原創(chuàng)文章&經(jīng)驗(yàn)總結(jié)&從校招到A廠一路陽光一路滄桑

詳情請戳www.codercc.com

image

1. final的簡介

final可以修飾變量,方法和類,用于表示所修飾的內(nèi)容一旦賦值之后就不會再被改變匕荸,比如String類就是一個final類型的類。即使能夠知道final具體的使用方法枷邪,我想對final在多線程中存在的重排序問題也很容易忽略榛搔,希望能夠一起做下探討。

2. final的具體使用場景

final能夠修飾變量东揣,方法和類践惑,也就是final使用范圍基本涵蓋了java每個地方,下面就分別以鎖修飾的位置:變量嘶卧,方法和類分別來說一說尔觉。

2.1 變量

在java中變量,可以分為成員變量以及方法局部變量脸候。因此也是按照這種方式依次來說穷娱,以避免漏掉任何一個死角。

2.1.1 final成員變量

通常每個類中的成員變量可以分為類變量(static修飾的變量)以及實(shí)例變量运沦。針對這兩種類型的變量賦初值的時機(jī)是不同的泵额,類變量可以在聲明變量的時候直接賦初值或者在靜態(tài)代碼塊中給類變量賦初值。而實(shí)例變量可以在聲明變量的時候給實(shí)例變量賦初值携添,在非靜態(tài)初始化塊中以及構(gòu)造器中賦初值嫁盲。類變量有兩個時機(jī)賦初值,而實(shí)例變量則可以有三個時機(jī)賦初值。當(dāng)final變量未初始化時系統(tǒng)不會進(jìn)行隱式初始化羞秤,會出現(xiàn)報錯缸托。這樣說起來還是比較抽象,下面用具體的代碼來演示瘾蛋。(代碼涵蓋了final修飾變量所有的可能情況俐镐,耐心看下去會有收獲的:) )

final修飾成員變量

看上面的圖片已經(jīng)將每種情況整理出來了,這里用截圖的方式也是覺得在IDE出現(xiàn)紅色出錯的標(biāo)記更能清晰的說明情況〔负撸現(xiàn)在我們來將這幾種情況歸納整理一下:

  1. 類變量:必須要在靜態(tài)初始化塊中指定初始值或者聲明該類變量時指定初始值佩抹,而且只能在這兩個地方之一進(jìn)行指定;
  2. 實(shí)例變量:必要要在非靜態(tài)初始化塊取董,聲明該實(shí)例變量或者在構(gòu)造器中指定初始值棍苹,而且只能在這三個地方進(jìn)行指定。

2.2.2 final局部變量

final局部變量由程序員進(jìn)行顯式初始化茵汰,如果final局部變量已經(jīng)進(jìn)行了初始化則后面就不能再次進(jìn)行更改枢里,如果final變量未進(jìn)行初始化,可以進(jìn)行賦值蹂午,當(dāng)且僅有一次賦值栏豺,一旦賦值之后再次賦值就會出錯。下面用具體的代碼演示final局部變量的情況:

final修飾局部變量

現(xiàn)在我們來換一個角度進(jìn)行考慮画侣,final修飾的是基本數(shù)據(jù)類型和引用類型有區(qū)別嗎冰悠?

final基本數(shù)據(jù)類型 VS final引用數(shù)據(jù)類型

通過上面的例子我們已經(jīng)看出來堡妒,如果final修飾的是一個基本數(shù)據(jù)類型的數(shù)據(jù)配乱,一旦賦值后就不能再次更改,那么皮迟,如果final是引用數(shù)據(jù)類型了搬泥?這個引用的對象能夠改變嗎?我們同樣來看一段代碼伏尼。

public class FinalExample {
    //在聲明final實(shí)例成員變量時進(jìn)行賦值
    private final static Person person = new Person(24, 170);
    public static void main(String[] args) {
        //對final引用數(shù)據(jù)類型person進(jìn)行更改
        person.age = 22;
        System.out.println(person.toString());
    }
    static class Person {
        private int age;
        private int height;

        public Person(int age, int height) {
            this.age = age;
            this.height = height;
        }
        @Override
        public String toString() {
            return "Person{" +
                    "age=" + age +
                    ", height=" + height +
                    '}';
        }
    }
}

當(dāng)我們對final修飾的引用數(shù)據(jù)類型變量person的屬性改成22忿檩,是可以成功操作的。通過這個實(shí)驗(yàn)我們就可以看出來當(dāng)final修飾基本數(shù)據(jù)類型變量時爆阶,不能對基本數(shù)據(jù)類型變量重新賦值燥透,因此基本數(shù)據(jù)類型變量不能被改變。而對于引用類型變量而言辨图,它僅僅保存的是一個引用班套,final只保證這個引用類型變量所引用的地址不會發(fā)生改變,即一直引用這個對象故河,但這個對象屬性是可以改變的吱韭。

宏變量

利用final變量的不可更改性,在滿足一下三個條件時鱼的,該變量就會成為一個“宏變量”理盆,即是一個常量痘煤。

  1. 使用final修飾符修飾;
  2. 在定義該final變量時就指定了初始值猿规;
  3. 該初始值在編譯時就能夠唯一指定衷快。

注意:當(dāng)程序中其他地方使用該宏變量的地方,編譯器會直接替換成該變量的值

2.2 方法

重寫姨俩?

當(dāng)父類的方法被final修飾的時候烦磁,子類不能重寫父類的該方法,比如在Object中哼勇,getClass()方法就是final的都伪,我們就不能重寫該方法,但是hashCode()方法就不是被final所修飾的积担,我們就可以重寫hashCode()方法陨晶。我們還是來寫一個例子來加深一下理解:
先定義一個父類,里面有final修飾的方法test();

public class FinalExampleParent {
    public final void test() {
    }
}

然后FinalExample繼承該父類帝璧,當(dāng)重寫test()方法時出現(xiàn)報錯先誉,如下圖:

final方法不能重寫

通過這個現(xiàn)象我們就可以看出來被final修飾的方法不能夠被子類所重寫

重載的烁?

public class FinalExampleParent {
    public final void test() {
    }

    public final void test(String str) {
    }
}

可以看出被final修飾的方法是可以重載的褐耳。經(jīng)過我們的分析可以得出如下結(jié)論:

1. 父類的final方法是不能夠被子類重寫的

2. final方法是可以被重載的

2.3 類

當(dāng)一個類被final修飾時,表名該類是不能被子類繼承的渴庆。子類繼承往往可以重寫父類的方法和改變父類屬性铃芦,會帶來一定的安全隱患,因此襟雷,當(dāng)一個類不希望被繼承時就可以使用final修飾刃滓。還是來寫一個小例子:

public final class FinalExampleParent {
    public final void test() {
    }
}

父類會被final修飾,當(dāng)子類繼承該父類的時候耸弄,就會報錯咧虎,如下圖:

final類不能繼承

3. final的例子

final經(jīng)常會被用作不變類上,利用final的不可更改性计呈。我們先來看看什么是不變類砰诵。

不變類

不變類的意思是創(chuàng)建該類的實(shí)例后,該實(shí)例的實(shí)例變量是不可改變的捌显。滿足以下條件則可以成為不可變類:

  1. 使用private和final修飾符來修飾該類的成員變量
  2. 提供帶參的構(gòu)造器用于初始化類的成員變量茁彭;
  3. 僅為該類的成員變量提供getter方法,不提供setter方法苇瓣,因?yàn)槠胀ǚ椒o法修改fina修飾的成員變量尉间;
  4. 如果有必要就重寫Object類 的hashCode()和equals()方法,應(yīng)該保證用equals()判斷相同的兩個對象其Hashcode值也是相等的。

JDK中提供的八個包裝類和String類都是不可變類哲嘲,我們來看看String的實(shí)現(xiàn)贪薪。

/** The value is used for character storage. */
 private final char value[];

可以看出String的value就是final修飾的,上述其他幾條性質(zhì)也是吻合的眠副。

4. 多線程中你真的了解final嗎画切?

上面我們聊的final使用,應(yīng)該屬于Java基礎(chǔ)層面的囱怕,當(dāng)理解這些后我們就真的算是掌握了final嗎霍弹?有考慮過final在多線程并發(fā)的情況嗎?在java內(nèi)存模型中我們知道java內(nèi)存模型為了能讓處理器和編譯器底層發(fā)揮他們的最大優(yōu)勢娃弓,對底層的約束就很少典格,也就是說針對底層來說java內(nèi)存模型就是一弱內(nèi)存數(shù)據(jù)模型。同時台丛,處理器和編譯為了性能優(yōu)化會對指令序列有編譯器和處理器重排序耍缴。那么,在多線程情況下,final會進(jìn)行怎樣的重排序挽霉?會導(dǎo)致線程安全的問題嗎防嗡?下面,就來看看final的重排序侠坎。

4.1 final域重排序規(guī)則

4.1.1 final域?yàn)榛绢愋?/h3>

先看一段示例性的代碼:

public class FinalDemo {
    private int a;  //普通域
    private final int b; //final域
    private static FinalDemo finalDemo;

    public FinalDemo() {
        a = 1; // 1. 寫普通域
        b = 2; // 2. 寫final域
    }

    public static void writer() {
        finalDemo = new FinalDemo();
    }

    public static void reader() {
        FinalDemo demo = finalDemo; // 3.讀對象引用
        int a = demo.a;    //4.讀普通域
        int b = demo.b;    //5.讀final域
    }
}

假設(shè)線程A在執(zhí)行writer()方法蚁趁,線程B執(zhí)行reader()方法。

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

寫final域的重排序規(guī)則禁止對final域的寫重排序到構(gòu)造函數(shù)之外实胸,這個規(guī)則的實(shí)現(xiàn)主要包含了兩個方面:

  1. JMM禁止編譯器把final域的寫重排序到構(gòu)造函數(shù)之外他嫡;
  2. 編譯器會在final域?qū)懼螅瑯?gòu)造函數(shù)return之前童芹,插入一個storestore屏障(關(guān)于內(nèi)存屏障可以看這篇文章)涮瞻。這個屏障可以禁止處理器把final域的寫重排序到構(gòu)造函數(shù)之外鲤拿。

我們再來分析writer方法假褪,雖然只有一行代碼,但實(shí)際上做了兩件事情:

  1. 構(gòu)造了一個FinalDemo對象近顷;
  2. 把這個對象賦值給成員變量finalDemo生音。

我們來畫下存在的一種可能執(zhí)行時序圖,如下:

final域?qū)懣赡艿拇嬖诘膱?zhí)行時序

由于a,b之間沒有數(shù)據(jù)依賴性窒升,普通域(普通變量)a可能會被重排序到構(gòu)造函數(shù)之外缀遍,線程B就有可能讀到的是普通變量a初始化之前的值(零值),這樣就可能出現(xiàn)錯誤饱须。而final域變量b域醇,根據(jù)重排序規(guī)則,會禁止final修飾的變量b重排序到構(gòu)造函數(shù)之外,從而b能夠正確賦值譬挚,線程B就能夠讀到final變量初始化后的值锅铅。

因此,寫final域的重排序規(guī)則可以確保:在對象引用為任意線程可見之前减宣,對象的final域已經(jīng)被正確初始化過了盐须,而普通域就不具有這個保障。比如在上例漆腌,線程B有可能就是一個未正確初始化的對象finalDemo贼邓。

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

讀final域重排序規(guī)則為:在一個線程中,初次讀對象引用和初次讀該對象包含的final域闷尿,JMM會禁止這兩個操作的重排序塑径。(注意,這個規(guī)則僅僅是針對處理器)填具,處理器會在讀final域操作的前面插入一個LoadLoad屏障晓勇。實(shí)際上,讀對象的引用和讀該對象的final域存在間接依賴性灌旧,一般處理器不會重排序這兩個操作绑咱。但是有一些處理器會重排序,因此枢泰,這條禁止重排序規(guī)則就是針對這些處理器而設(shè)定的描融。

read()方法主要包含了三個操作:

  1. 初次讀引用變量finalDemo;
  2. 初次讀引用變量finalDemo的普通域a;
  3. 初次讀引用變量finalDemo的final與b;

假設(shè)線程A寫過程沒有重排序,那么線程A和線程B有一種的可能執(zhí)行時序?yàn)橄聢D:

final域讀可能存在的執(zhí)行時序

讀對象的普通域被重排序到了讀對象引用的前面就會出現(xiàn)線程B還未讀到對象引用就在讀取該對象的普通域變量衡蚂,這顯然是錯誤的操作窿克。而final域的讀操作就“限定”了在讀final域變量前已經(jīng)讀到了該對象的引用,從而就可以避免這種情況毛甲。

讀final域的重排序規(guī)則可以確保:在讀一個對象的final域之前年叮,一定會先讀這個包含這個final域的對象的引用。

4.1.2 final域?yàn)橐妙愋?/h3>

我們已經(jīng)知道了final域是基本數(shù)據(jù)類型的時候重排序規(guī)則是怎么的了玻募?如果是引用數(shù)據(jù)類型了只损?我們接著繼續(xù)來探討。

對final修飾的對象的成員域?qū)懖僮?/strong>

針對引用數(shù)據(jù)類型七咧,final域?qū)戓槍幾g器和處理器重排序增加了這樣的約束:在構(gòu)造函數(shù)內(nèi)對一個final修飾的對象的成員域的寫入跃惫,與隨后在構(gòu)造函數(shù)之外把這個被構(gòu)造的對象的引用賦給一個引用變量,這兩個操作是不能被重排序的艾栋。注意這里的是“增加”也就說前面對final基本數(shù)據(jù)類型的重排序規(guī)則在這里還是使用爆存。這句話是比較拗口的,下面結(jié)合實(shí)例來看蝗砾。

public class FinalReferenceDemo {
    final int[] arrays;
    private FinalReferenceDemo finalReferenceDemo;

    public FinalReferenceDemo() {
        arrays = new int[1];  //1
        arrays[0] = 1;        //2
    }

    public void writerOne() {
        finalReferenceDemo = new FinalReferenceDemo(); //3
    }

    public void writerTwo() {
        arrays[0] = 2;  //4
    }

    public void reader() {
        if (finalReferenceDemo != null) {  //5
            int temp = finalReferenceDemo.arrays[0];  //6
        }
    }
}

針對上面的實(shí)例程序先较,線程線程A執(zhí)行wirterOne方法携冤,執(zhí)行完后線程B執(zhí)行writerTwo方法,然后線程C執(zhí)行reader方法闲勺。下圖就以這種執(zhí)行時序出現(xiàn)的一種情況來討論(耐心看完才有收獲)噪叙。

寫final修飾引用類型數(shù)據(jù)可能的執(zhí)行時序

由于對final域的寫禁止重排序到構(gòu)造方法外,因此1和3不能被重排序霉翔。由于一個final域的引用對象的成員域?qū)懭氩荒芘c隨后將這個被構(gòu)造出來的對象賦給引用變量重排序睁蕾,因此2和3不能重排序。

對final修飾的對象的成員域讀操作

JMM可以確保線程C至少能看到寫線程A對final引用的對象的成員域的寫入债朵,即能看下arrays[0] = 1子眶,而寫線程B對數(shù)組元素的寫入可能看到可能看不到。JMM不保證線程B的寫入對線程C可見序芦,線程B和線程C之間存在數(shù)據(jù)競爭臭杰,此時的結(jié)果是不可預(yù)知的。如果可見的谚中,可使用鎖或者volatile渴杆。

關(guān)于final重排序的總結(jié)

按照final修飾的數(shù)據(jù)類型分類:

基本數(shù)據(jù)類型:

  1. final域?qū)懀航?strong>final域?qū)?/strong>與構(gòu)造方法重排序,即禁止final域?qū)懼嘏判虻綐?gòu)造方法之外宪塔,從而保證該對象對所有線程可見時磁奖,該對象的final域全部已經(jīng)初始化過。
  2. final域讀:禁止初次讀對象的引用讀該對象包含的final域的重排序某筐。

引用數(shù)據(jù)類型:

額外增加約束:禁止在構(gòu)造函數(shù)對一個final修飾的對象的成員域的寫入與隨后將這個被構(gòu)造的對象的引用賦值給引用變量 重排序

5.final的實(shí)現(xiàn)原理

上面我們提到過比搭,寫final域會要求編譯器在final域?qū)懼螅瑯?gòu)造函數(shù)返回前插入一個StoreStore屏障南誊。讀final域的重排序規(guī)則會要求編譯器在讀final域的操作前插入一個LoadLoad屏障身诺。

很有意思的是,如果以X86處理為例抄囚,X86不會對寫-寫重排序霉赡,所以StoreStore屏障可以省略。由于不會對有間接依賴性的操作重排序幔托,所以在X86處理器中穴亏,讀final域需要的LoadLoad屏障也會被省略掉。也就是說柑司,以X86為例的話迫肖,對final域的讀/寫的內(nèi)存屏障都會被省略!具體是否插入還是得看是什么處理器

6. 為什么final引用不能從構(gòu)造函數(shù)中“溢出”

這里還有一個比較有意思的問題:上面對final域?qū)懼嘏判蛞?guī)則可以確保我們在使用一個對象引用的時候該對象的final域已經(jīng)在構(gòu)造函數(shù)被初始化過了攒驰。但是這里其實(shí)是有一個前提條件的,也就是:在構(gòu)造函數(shù)故爵,不能讓這個被構(gòu)造的對象被其他線程可見玻粪,也就是說該對象引用不能在構(gòu)造函數(shù)中“逸出”隅津。以下面的例子來說:

public class FinalReferenceEscapeDemo {
    private final int a;
    private FinalReferenceEscapeDemo referenceDemo;

    public FinalReferenceEscapeDemo() {
        a = 1;  //1
        referenceDemo = this; //2
    }

    public void writer() {
        new FinalReferenceEscapeDemo();
    }

    public void reader() {
        if (referenceDemo != null) {  //3
            int temp = referenceDemo.a; //4
        }
    }
}

可能的執(zhí)行時序如圖所示:

final域引用可能的執(zhí)行時序

假設(shè)一個線程A執(zhí)行writer方法另一個線程執(zhí)行reader方法。因?yàn)闃?gòu)造函數(shù)中操作1和2之間沒有數(shù)據(jù)依賴性劲室,1和2可以重排序伦仍,先執(zhí)行了2,這個時候引用對象referenceDemo是個沒有完全初始化的對象很洋,而當(dāng)線程B去讀取該對象時就會出錯充蓝。盡管依然滿足了final域?qū)懼嘏判蛞?guī)則:在引用對象對所有線程可見時,其final域已經(jīng)完全初始化成功喉磁。但是谓苟,引用對象“this”逸出,該代碼依然存在線程安全的問題协怒。

參看文獻(xiàn)

《java并發(fā)編程的藝術(shù)》
《瘋狂java講義》

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末涝焙,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子孕暇,更是在濱河造成了極大的恐慌仑撞,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件妖滔,死亡現(xiàn)場離奇詭異隧哮,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)座舍,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進(jìn)店門近迁,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人簸州,你說我怎么就攤上這事鉴竭。” “怎么了岸浑?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵搏存,是天一觀的道長。 經(jīng)常有香客問我矢洲,道長璧眠,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任读虏,我火速辦了婚禮责静,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘盖桥。我一直安慰自己灾螃,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布揩徊。 她就那樣靜靜地躺著腰鬼,像睡著了一般嵌赠。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上熄赡,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天姜挺,我揣著相機(jī)與錄音,去河邊找鬼彼硫。 笑死炊豪,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的拧篮。 我是一名探鬼主播词渤,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼他托!你這毒婦竟也來了掖肋?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤赏参,失蹤者是張志新(化名)和其女友劉穎志笼,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體把篓,經(jīng)...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡纫溃,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了韧掩。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片紊浩。...
    茶點(diǎn)故事閱讀 37,997評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖疗锐,靈堂內(nèi)的尸體忽然破棺而出坊谁,到底是詐尸還是另有隱情,我是刑警寧澤滑臊,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布口芍,位于F島的核電站,受9級特大地震影響雇卷,放射性物質(zhì)發(fā)生泄漏鬓椭。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一关划、第九天 我趴在偏房一處隱蔽的房頂上張望小染。 院中可真熱鬧,春花似錦贮折、人聲如沸裤翩。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽岛都。三九已至律姨,卻和暖如春振峻,著一層夾襖步出監(jiān)牢的瞬間臼疫,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工扣孟, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留烫堤,地道東北人。 一個月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓凤价,卻偏偏與公主長得像鸽斟,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子利诺,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,722評論 2 345

推薦閱讀更多精彩內(nèi)容