final實作原理

簡介
final關鍵字,實際的含義就一句話,不可改變,什么是不可改變?就是初始化完成之后就不能再做任何的修改,修飾成員變數的時候,成員變數變成一個常數;修飾方法的時候,方法不允許被重寫;修飾類的時候,類不允許被繼承;修飾引數串列的時候,入參的物件也是不可以改變,這個就是不可變,無論是參考新的物件,重寫還是繼承,都是改變的方法,而final就是把這個變更的路給堵死
用法
final修飾變數
- final成員變數表示常量,只能被賦值一次,賦值后值不再改變(final要求地址值不能改變)
- 當final修飾一個基本資料型別時,表示該基本資料型別的值一旦在初始化后便不能發生變化;
- 如果final修飾一個參考型別時,則在對其初始化之后便不能再讓其指向其他物件了,但該參考所指向的物件的內容是可以發生變化的,本質上是一回事,因為參考的值是一個地址,final要求值,即地址的值不發生變化,
- final修飾一個成員變數(屬性),必須要顯示初始化,這里有兩種初始化方式,
- 一種是在變數宣告的時候初始化,
- 第二種方法是在宣告變數的時候不賦初值,但是要在這個變數所在的類的所有的建構式中對這個變數賦初值,
final修飾方法
使用final方法的原因有兩個,
- 第一個原因是把方法鎖定,以防任何繼承類修改它的含義,不能被重寫;
- 第二個原因是效率,final方法比非final方法要快,因為在編譯的時候已經靜態系結了,不需要在運行時再動態系結,
注:類的private方法會隱式地被指定為final方法
final修飾類
當用final修飾一個類時,表明這個類不能被繼承,
final類中的成員變數可以根據需要設為final,但是要注意final類中的所有成員方法都會被隱式地指定為final方法,
在使用final修飾類的時候,要注意謹慎選擇,除非這個類真的在以后不會用來繼承或者出于安全的考慮,盡量不要將類設計為final類,
final關鍵字的好處
- final關鍵字提高了性能,JVM和Java應用都會快取final變數,
- final變數可以安全的在多執行緒環境下進行共享,而不需要額外的同步開銷,
- 使用final關鍵字,JVM會對方法、變數及類進行優化,
注意事項
- final關鍵字可以用于成員變數、本地變數、方法以及類,
- final成員變數必須在宣告的時候初始化或者在構造器中初始化,否則就會報編譯錯誤,
- 你不能夠對final變數再次賦值,
- 本地變數必須在宣告時賦值,
- 在匿名類中所有變數都必須是final變數,
- final方法不能被重寫,
- final類不能被繼承,
- final關鍵字不同于finally關鍵字,后者用于例外處理,
- final關鍵字容易與finalize()方法搞混,后者是在Object類中定義的方法,是在垃圾回收之前被JVM呼叫的方法,
- 介面中宣告的所有變數本身是final的,
- final和abstract這兩個關鍵字是反相關的,final類就不可能是abstract的,
- final方法在編譯階段系結,稱為靜態系結(static binding),
- 沒有在宣告時初始化final變數的稱為空白final變數(blank final variable),它們必須在構造器中初始化,或者呼叫this()初始化,不這么做的話,編譯器會報錯“final變數(變數名)需要進行初始化”,
- 將類、方法、變數宣告為final能夠提高性能,這樣JVM就有機會進行估計,然后優化,
- 按照Java代碼慣例,final變數就是常量,而且通常常量名要大寫,
- 對于集合物件宣告為final指的是參考不能被更改,但是你可以向其中增加,洗掉或者改變內容,
原理
記憶體語意
寫記憶體語意可以確保在物件的參考為任意執行緒可見之前,final 域已經被初始化過了,
讀記憶體語意可以確保如果物件的參考不為 null,則說明 final 域已經被初始化過了,
總之,final 域的記憶體語意提供了初始化安全保證,
- 寫記憶體語意:在建構式內對一個 final 域的寫入,與隨后將物件參考賦值給參考變數,這兩個操作不能重排序,
- 讀記憶體語意:初次讀一個包含 final 域的物件的參考,與隨后初次讀這個 final 域,這兩個操作不能重排序,
寫 final 域的重排序規則
寫 final 域的重排序規則禁止把 final 域的寫重排序到建構式之外,這個規則的實作包含下面 2 個方面:
- JMM 禁止編譯器把 final 域的寫重排序到建構式之外,
- 編譯器會在 final 域的寫之后,建構式 return 之前,插入一個 StoreStore 屏障,這個屏障禁止處理器把 final 域的寫重排序到建構式之外,
現在讓我們分析 writer () 方法,writer () 方法只包含一行代碼:finalExample = new FinalExample (),這行代碼包含兩個步驟:
- 構造一個 FinalExample 型別的物件;
- 把這個物件的參考賦值給參考變數 obj,
假設執行緒 B 讀物件參考與讀物件的成員域之間沒有重排序(馬上會說明為什么需要這個假設),下圖是一種可能的執行時序:

在上圖中,寫普通域的操作被編譯器重排序到了建構式之外,讀執行緒 B 錯誤的讀取了普通變數 i 初始化之前的值,而寫 final 域的操作,被寫 final 域的重排序規則“限定”在了建構式之內,讀執行緒 B 正確的讀取了 final 變數初始化之后的值,
寫 final 域的重排序規則可以確保:在物件參考為任意執行緒可見之前,物件的 final 域已經被正確初始化過了,而普通域不具有這個保障,以上圖為例,在讀執行緒 B“看到”物件參考 obj 時,很可能 obj 物件還沒有構造完成(對普通域 i 的寫操作被重排序到建構式外,此時初始值 2 還沒有寫入普通域 i),
讀 final 域的重排序規則
讀 final 域的重排序規則如下:
在一個執行緒中,初次讀物件參考與初次讀該物件包含的 final 域,JMM 禁止處理器重排序這兩個操作(注意,這個規則僅僅針對處理器),編譯器會在讀 final 域操作的前面插入一個 LoadLoad 屏障,
初次讀物件參考與初次讀該物件包含的 final 域,這兩個操作之間存在間接依賴關系,由于編譯器遵守間接依賴關系,因此編譯器不會重排序這兩個操作,大多數處理器也會遵守間接依賴,大多數處理器也不會重排序這兩個操作,但有少數處理器允許對存在間接依賴關系的操作做重排序(比如 alpha 處理器),這個規則就是專門用來針對這種處理器,
reader() 方法包含三個操作:
- 初次讀參考變數 obj;
- 初次讀參考變數 obj 指向物件的普通域 j,
- 初次讀參考變數 obj 指向物件的 final 域 i
現在我們假設寫執行緒 A 沒有發生任何重排序,同時程式在不遵守間接依賴的處理器上執行,下面是一種可能的執行時序

在上圖中,讀物件的普通域的操作被處理器重排序到讀物件參考之前,讀普通域時,該域還沒有被寫執行緒 A 寫入,這是一個錯誤的讀取操作,而讀 final 域的重排序規則會把讀物件 final 域的操作“限定”在讀物件參考之后,此時該 final 域已經被 A 執行緒初始化過了,這是一個正確的讀取操作,
讀 final 域的重排序規則可以確保:在讀一個物件的 final 域之前,一定會先讀包含這個 final 域的物件的參考,在這個示例程式中,如果該參考不為 null,那么參考物件的 final 域一定已經被 A 執行緒初始化過了,
如果 final 域是參考型別
上面我們看到的 final 域是基礎資料型別,下面讓我們看看如果 final 域是參考型別,將會有什么效果?
請看下列示例代碼:
COPYpublic class FinalReferenceExample {
final int[] intArray; //final 是參考型別
static FinalReferenceExample obj;
public FinalReferenceExample () { // 建構式
intArray = new int[1]; //1
intArray[0] = 1; //2
}
public static void writerOne () { // 寫執行緒 A 執行
obj = new FinalReferenceExample (); //3
}
public static void writerTwo () { // 寫執行緒 B 執行
obj.intArray[0] = 2; //4
}
public static void reader () { // 讀執行緒 C 執行
if (obj != null) { //5
int temp1 = obj.intArray[0]; //6
}
}
}
這里 final 域為一個參考型別,它參考一個 int 型的陣列物件,對于參考型別,寫 final 域的重排序規則對編譯器和處理器增加了如下約束:
在建構式內對一個 final 參考的物件的成員域的寫入,與隨后在建構式外把這個被構造物件的參考賦值給一個參考變數,這兩個操作之間不能重排序,
對上面的示例程式,我們假設首先執行緒 A 執行 writerOne() 方法,執行完后執行緒 B 執行 writerTwo() 方法,執行完后執行緒 C 執行 reader () 方法,下面是一種可能的執行緒執行時序:

在上圖中,1 是對 final 域的寫入,2 是對這個 final 域參考的物件的成員域的寫入,3 是把被構造的物件的參考賦值給某個參考變數,這里除了前面提到的 1 不能和 3 重排序外,2 和 3 也不能重排序,
JMM 可以確保讀執行緒 C 至少能看到寫執行緒 A 在建構式中對 final 參考物件的成員域的寫入,即 C 至少能看到陣列下標 0 的值為 1,而寫執行緒 B 對陣列元素的寫入,讀執行緒 C 可能看的到,也可能看不到,JMM 不保證執行緒 B 的寫入對讀執行緒 C 可見,因為寫執行緒 B 和讀執行緒 C 之間存在資料競爭,此時的執行結果不可預知,
如果想要確保讀執行緒 C 看到寫執行緒 B 對陣列元素的寫入,寫執行緒 B 和讀執行緒 C 之間需要使用同步原語(lock 或 volatile)來確保記憶體可見性,
為什么 final 參考不能從建構式內“逸出”
前面我們提到過,寫 final 域的重排序規則可以確保:在參考變數為任意執行緒可見之前,該參考變數指向的物件的 final 域已經在建構式中被正確初始化過了,其實要得到這個效果,還需要一個保證:在建構式內部,不能讓這個被構造物件的參考為其他執行緒可見,也就是物件參考不能在建構式中“逸出”,為了說明問題,讓我們來看下面示例代碼:
COPYpublic class FinalReferenceEscapeExample {
final int i;
static FinalReferenceEscapeExample obj;
public FinalReferenceEscapeExample () {
i = 1; //1 寫 final 域
obj = this; //2 this 參考在此“逸出”
}
public static void writer() {
new FinalReferenceEscapeExample ();
}
public static void reader {
if (obj != null) { //3
int temp = obj.i; //4
}
}
}
假設一個執行緒 A 執行 writer() 方法,另一個執行緒 B 執行 reader() 方法,這里的操作 2 使得物件還未完成構造前就為執行緒 B 可見,即使這里的操作 2 是建構式的最后一步,且即使在程式中操作 2 排在操作 1 后面,執行 read() 方法的執行緒仍然可能無法看到 final 域被初始化后的值,因為這里的操作 1 和操作 2 之間可能被重排序,實際的執行時序可能如下圖所示:

從上圖我們可以看出:在建構式回傳前,被構造物件的參考不能為其他執行緒可見,因為此時的 final 域可能還沒有被初始化,在建構式回傳后,任意執行緒都將保證能看到 final 域正確初始化之后的值,
final 語意在處理器中的實作
現在我們以 x86 處理器為例,說明 final 語意在處理器中的具體實作,
上面我們提到,寫 final 域的重排序規則會要求譯編器在 final 域的寫之后,建構式 return 之前,插入一個 StoreStore 障屏,讀 final 域的重排序規則要求編譯器在讀 final 域的操作前面插入一個 LoadLoad 屏障,
由于 x86 處理器不會對寫 - 寫操作做重排序,所以在 x86 處理器中,寫 final 域需要的 StoreStore 障屏會被省略掉,同樣,由于 x86 處理器不會對存在間接依賴關系的操作做重排序,所以在 x86 處理器中,讀 final 域需要的 LoadLoad 屏障也會被省略掉,也就是說在 x86 處理器中,final 域的讀 / 寫不會插入任何記憶體屏障!
為什么要增強 final 的語意
在舊的 Java 記憶體模型中 ,最嚴重的一個缺陷就是執行緒可能看到 final 域的值會改變,比如,一個執行緒當前看到一個整形 final 域的值為 0(還未初始化之前的默認值),過一段時間之后這個執行緒再去讀這個 final 域的值時,卻發現值變為了 1(被某個執行緒初始化之后的值),最常見的例子就是在舊的 Java 記憶體模型中,String 的值可能會改變,
為了修補這個漏洞,JSR-133 專家組增強了 final 的語意,通過為 final 域增加寫和讀重排序規則,可以為 java 程式員提供初始化安全保證:只要物件是正確構造的(被構造物件的參考在建構式中沒有“逸出”),那么不需要使用同步(指 lock 和 volatile 的使用),就可以保證任意執行緒都能看到這個 final 域在建構式中被初始化之后的值,
final、finally、 finalize區別
- final可以用來修飾類、方法、變數,分別有不同的意義,final修飾的class代表不可以繼承擴展,final的變數是不可以修改的,而final的方法也是不可以重寫的(override),
- finally則是Java保證重點代碼一定要被執行的一種機制,我們可以使用try-finally或者try-catch-finally來進行類似關閉JDBC連接、保證unlock鎖等動作,
- finalize是基礎類java.lang.Object的一個方法,它的設計目的是保證物件在被垃圾收集前完成特定資源的回收,finalize機制現在已經不推薦使用,并且在JDK 9開始被標記為deprecated,
本文由
傳智教育博學谷狂野架構師教研團隊發布,如果本文對您有幫助,歡迎
關注和點贊;如果您有任何建議也可留言評論或私信,您的支持是我堅持創作的動力,轉載請注明出處!
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/545522.html
標籤:Java
上一篇:Java 根據模板匯出PDF
