溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

你以為你真的了解final嗎?

發(fā)布時間:2020-06-18 03:33:39 來源:網(wǎng)絡 閱讀:175 作者:Java筆記丶 欄目:編程語言

本人免費整理了Java高級資料,涵蓋了Java、Redis、MongoDB、MySQL、Zookeeper、Spring Cloud、Dubbo高并發(fā)分布式等教程,一共30G,需要自己領取。
傳送門:https://mp.weixin.qq.com/s/JzddfH-7yNudmkjT0IRL8Q

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


2. final的具體使用場景
final能夠修飾變量,方法和類,也就是final使用范圍基本涵蓋了java每個地方,下面就分別以鎖修飾的位置:變量,方法和類分別來說一說。


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


2.1.1 final成員變量
通常每個類中的成員變量可以分為類變量(static修飾的變量)以及實例變量。針對這兩種類型的變量賦初值的時機是不同的,類變量可以在聲明變量的時候直接賦初值或者在靜態(tài)代碼塊中給類變量賦初值。

而實例變量可以在聲明變量的時候給實例變量賦初值,在非靜態(tài)初始化塊中以及構造器中賦初值。類變量有兩個時機賦初值,而實例變量則可以有三個時機賦初值。

當final變量未初始化時系統(tǒng)不會進行隱式初始化,會出現(xiàn)報錯。這樣說起來還是比較抽象,下面用具體的代碼來演示。(代碼涵蓋了final修飾變量所有的可能情況,耐心看下去會有收獲的:) )

你以為你真的了解final嗎?


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

  1. 類變量:必須要在靜態(tài)初始化塊中指定初始值或者聲明該類變量時指定初始值,而且只能在這兩個地方之一進行指定;

  2. 實例變量:必要要在非靜態(tài)初始化塊,聲明該實例變量或者在構造器中指定初始值,而且只能在這三個地方進行指定。

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

你以為你真的了解final嗎?


現(xià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實例成員變量時進行賦值????private?final?static?Person?person?=?new?Person(24,?170);
????public?static?void?main(String[]?args)?{
????????//對final引用數(shù)據(jù)類型person進行更改????????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?+
????????????????????'}';
????????}
????}}


當我們對final修飾的引用數(shù)據(jù)類型變量person的屬性改成22,是可以成功操作的。通過這個實驗我們就可以看出來當final修飾基本數(shù)據(jù)類型變量時,不能對基本數(shù)據(jù)類型變量重新賦值,因此基本數(shù)據(jù)類型變量不能被改變。

而對于引用類型變量而言,它僅僅保存的是一個引用,final只保證這個引用類型變量所引用的地址不會發(fā)生改變,即一直引用這個對象,但這個對象屬性是可以改變的

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

  1. 使用final修飾符修飾;

  2. 在定義該final變量時就指定了初始值;

  3. 該初始值在編譯時就能夠唯一指定。

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

2.2 方法
重寫?
當父類的方法被final修飾的時候,子類不能重寫父類的該方法,比如在Object中,getClass()方法就是final的,我們就不能重寫該方法,但是hashCode()方法就不是被final所修飾的,我們就可以重寫hashCode()方法。我們還是來寫一個例子來加深一下理解: 先定義一個父類,里面有final修飾的方法test();


public class FinalExampleParent { public final void test() { } } 復制代碼
然后FinalExample繼承該父類,當重寫test()方法時出現(xiàn)報錯,如下圖:

你以為你真的了解final嗎?


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

重載?

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

????public?final?void?test(String?str)?{
????}}


可以看出被final修飾的方法是可以重載的。經(jīng)過我們的分析可以得出如下結論:
1. 父類的final方法是不能夠被子類重寫的
2. final方法是可以被重載的


2.3 類
當一個類被final修飾時,表名該類是不能被子類繼承的。子類繼承往往可以重寫父類的方法和改變父類屬性,會帶來一定的安全隱患,因此,當一個類不希望被繼承時就可以使用final修飾。還是來寫一個小例子:

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


父類會被final修飾,當子類繼承該父類的時候,就會報錯,如下圖:

你以為你真的了解final嗎?


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

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

  1. 使用private和final修飾符來修飾該類的成員變量

  2. 提供帶參的構造器用于初始化類的成員變量;

  3. 僅為該類的成員變量提供getter方法,不提供setter方法,因為普通方法無法修改fina修飾的成員變量;

  4. 如果有必要就重寫Object類 的hashCode()和equals()方法,應該保證用equals()判斷相同的兩個對象其Hashcode值也是相等的。

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

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

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


4. 多線程中你真的了解final嗎?
上面我們聊的final使用,應該屬于Java基礎層面的,當理解這些后我們就真的算是掌握了final嗎?有考慮過final在多線程并發(fā)的情況嗎?在java內(nèi)存模型中我們知道java內(nèi)存模型為了能讓處理器和編譯器底層發(fā)揮他們的最大優(yōu)勢,對底層的約束就很少,也就是說針對底層來說java內(nèi)存模型就是一弱內(nèi)存數(shù)據(jù)模型。

同時,處理器和編譯為了性能優(yōu)化會對指令序列有編譯器和處理器重排序。那么,在多線程情況下,final會進行怎樣的重排序?會導致線程安全的問題嗎?下面,就來看看final的重排序。


4.1 final域重排序規(guī)則
4.1.1 final域為基本類型


先看一段示例性的代碼:

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域
????}
}


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

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

寫final域的重排序規(guī)則禁止對final域的寫重排序到構造函數(shù)之外,這個規(guī)則的實現(xiàn)主要包含了兩個方面:
JMM禁止編譯器把final域的寫重排序到構造函數(shù)之外;

  1. 編譯器會在final域寫之后,構造函數(shù)return之前,插入一個storestore屏障。這個屏障可以禁止處理器把final域的寫重排序到構造函數(shù)之外。

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

  1. 構造了一個FinalDemo對象;

  2. 把這個對象賦值給成員變量finalDemo。

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

你以為你真的了解final嗎?


由于a,b之間沒有數(shù)據(jù)依賴性,普通域(普通變量)a可能會被重排序到構造函數(shù)之外,線程B就有可能讀到的是普通變量a初始化之前的值(零值),這樣就可能出現(xiàn)錯誤。

而final域變量b,根據(jù)重排序規(guī)則,會禁止final修飾的變量b重排序到構造函數(shù)之外,從而b能夠正確賦值,線程B就能夠讀到final變量初始化后的值。


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


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

實際上,讀對象的引用和讀該對象的final域存在間接依賴性,一般處理器不會重排序這兩個操作。但是有一些處理器會重排序,因此,這條禁止重排序規(guī)則就是針對這些處理器而設定的。
read()方法主要包含了三個操作:

  1. 初次讀引用變量finalDemo;

  2. 初次讀引用變量finalDemo的普通域a;

  3. 初次讀引用變量finalDemo的final與b;

假設線程A寫過程沒有重排序,那么線程A和線程B有一種的可能執(zhí)行時序為下圖:

你以為你真的了解final嗎?


讀對象的普通域被重排序到了讀對象引用的前面就會出現(xiàn)線程B還未讀到對象引用就在讀取該對象的普通域變量,這顯然是錯誤的操作。而final域的讀操作就“限定”了在讀final域變量前已經(jīng)讀到了該對象的引用,從而就可以避免這種情況。
讀final域的重排序規(guī)則可以確保:在讀一個對象的final域之前,一定會先讀這個包含這個final域的對象的引用。


4.1.2 final域為引用類型
我們已經(jīng)知道了final域是基本數(shù)據(jù)類型的時候重排序規(guī)則是怎么的了?如果是引用數(shù)據(jù)類型了?我們接著繼續(xù)來探討。
對final修飾的對象的成員域寫操作

針對引用數(shù)據(jù)類型,final域寫針對編譯器和處理器重排序增加了這樣的約束:在構造函數(shù)內(nèi)對一個final修飾的對象的成員域的寫入,與隨后在構造函數(shù)之外把這個被構造的對象的引用賦給一個引用變量,這兩個操作是不能被重排序的。

注意這里的是“增加”也就說前面對final基本數(shù)據(jù)類型的重排序規(guī)則在這里還是使用。這句話是比較拗口的,下面結合實例來看。

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????????}
????}}


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

你以為你真的了解final嗎?


由于對final域的寫禁止重排序到構造方法外,因此1和3不能被重排序。由于一個final域的引用對象的成員域寫入不能與隨后將這個被構造出來的對象賦給引用變量重排序,因此2和3不能重排序。


對final修飾的對象的成員域讀操作
JMM可以確保線程C至少能看到寫線程A對final引用的對象的成員域的寫入,即能看下arrays[0] = 1,而寫線程B對數(shù)組元素的寫入可能看到可能看不到。JMM不保證線程B的寫入對線程C可見,線程B和線程C之間存在數(shù)據(jù)競爭,此時的結果是不可預知的。如果可見的,可使用鎖或者volatile。


關于final重排序的總結
按照final修飾的數(shù)據(jù)類型分類:
基本數(shù)據(jù)類型:

  1. final域寫:禁止final域寫構造方法重排序,即禁止final域寫重排序到構造方法之外,從而保證該對象對所有線程可見時,該對象的final域全部已經(jīng)初始化過。

  2. final域讀:禁止初次讀對象的引用讀該對象包含的final域的重排序。

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


5.final的實現(xiàn)原理
上面我們提到過,寫final域會要求編譯器在final域寫之后,構造函數(shù)返回前插入一個StoreStore屏障。讀final域的重排序規(guī)則會要求編譯器在讀final域的操作前插入一個LoadLoad屏障。


很有意思的是,如果以X86處理為例,X86不會對寫-寫重排序,所以StoreStore屏障可以省略。由于不會對有間接依賴性的操作重排序,所以在X86處理器中,讀final域需要的LoadLoad屏障也會被省略掉。

也就是說,以X86為例的話,對final域的讀/寫的內(nèi)存屏障都會被省略!具體是否插入還是得看是什么處理器


6. 為什么final引用不能從構造函數(shù)中“溢出”
這里還有一個比較有意思的問題:上面對final域寫重排序規(guī)則可以確保我們在使用一個對象引用的時候該對象的final域已經(jīng)在構造函數(shù)被初始化過了。但是這里其實是有一個前提條件的,也就是:在構造函數(shù),不能讓這個被構造的對象被其他線程可見,也就是說該對象引用不能在構造函數(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嗎?


假設一個線程A執(zhí)行writer方法另一個線程執(zhí)行reader方法。因為構造函數(shù)中操作1和2之間沒有數(shù)據(jù)依賴性,1和2可以重排序,先執(zhí)行了2,這個時候引用對象referenceDemo是個沒有完全初始化的對象,而當線程B去讀取該對象時就會出錯。

盡管依然滿足了final域寫重排序規(guī)則:在引用對象對所有線程可見時,其final域已經(jīng)完全初始化成功。但是,引用對象“this”逸出,該代碼依然存在線程安全的問題。


向AI問一下細節(jié)

免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權內(nèi)容。

AI