final修飾符的底層原理
在 Java 中,final 修飾符的底層實現涉及 編譯器優化 和 JVM 字節碼層面的約束
其核心目標是保證被修飾元素的【不可變性】或 【不可重寫 / 繼承性】
一、final 修飾類:禁止繼承的底層約束
當一個類被 final 修飾時,例如 String 、Integer
JVM 在字節碼層面會通過 訪問標志(access flags) 標記該類為 ACC_FINAL
- 編譯器在編譯時會檢查:如果子類試圖繼承被 final 修飾的類,會直接拋出編譯錯誤
【無法繼承最終類】 - JVM 在類加載階段也會驗證這一約束,確保沒有非法繼承行為
本質:
- 通過字節碼標記禁止繼承
- 屬于編譯期和類加載期的靜態約束,不涉及運行時的特殊處理
二、final 修飾方法:禁止重寫的底層機制
final 修飾方法時,字節碼中該方法的訪問標志會被標記為 ACC_FINAL
- 編譯器在編譯子類時,若發現子類試圖重寫被 final 修飾的父類方法,會直接報錯
- JVM 在字節碼驗證階段也會檢查方法重寫的合法性,拒絕非法重寫的類加載
與 private 方法的區別:
隱式final:private 方法默認被隱式視為 final,但字節碼中不會標記 ACC_FINAL->方法無法被重寫,因為子類不可見
顯式 final :方法會明確標記,且可見性可以是 public/protected
三、final 修飾變量:保證不可變的底層實現
final 修飾變量(局部變量、成員變量、靜態變量)的核心是 【一旦賦值就不能被修改】
其底層實現涉及編譯期約束和運行期優化
分兩種情況:
1. 基本類型變量(如 final int a = 10)
- 編譯期約束:編譯器會檢查變量是否只被賦值一次。若在編譯期能確定賦值(如直接賦值字面量),則會將其視為 【編譯期常量】,并可能觸發 常量折疊 優化(如將代碼中所有引用 a 的地方直接替換為10)
- 運行期保障:若變量在編譯期無法確定值(如通過方法返回值賦值,final int a = getValue())
JVM 會在字節碼中通過 putfield(成員變量)或 astore(局部變量)指令賦值后,禁止后續對該變量的寫操作(編譯器會攔截所有二次賦值的代碼,直接報錯)
2. 引用類型變量(如 final List<String> list = new ArrayList<>())
final 對引用類型的約束是 【引用不可變】,但對象本身的內容可以修改(如 list.add("a") 是允許的)
- 底層通過字節碼標記 ACC_FINAL 實現:編譯器會檢查引用變量是否被二次賦值(如list = new LinkedList<>()),若有則編譯報錯
- 與基本類型不同,引用類型的 final 變量不會觸發常量折疊,因為其指向的對象內容可能在運行時變化,僅保證引用本身不變
3. final 與多線程: happens-before 規則的底層支持
final 變量在多線程環境中具有特殊的內存語義:被 final 修飾的變量,一旦在構造方法中初始化完成,且構造方法沒有 “逸出”(即 this 引用未被其他線程獲取),則其他線程看到的 final 變量一定是初始化后的值,無需額外同步
這一特性的底層依賴 JVM 的內存屏障:
- 在構造方法中對 final 變量賦值后,JVM 會插入 StoreStore 屏障,禁止該賦值操作與構造方法外的操作重排序,確保 final 變量的初始化對其他線程可見
- 其他線程讀取 final 變量時,JVM 會插入 LoadLoad 屏障,禁止讀取操作與之前的操作重排序,確保讀取到的是初始化后的值
四、final 與 JIT 優化:常量傳播與不可變分析
JIT(即時編譯器)在運行時會對 final 變量進行額外優化:
- 常量傳播:若 final 變量是編譯期常量(如 final int MAX = 100),JIT 會將代碼中所有引用 MAX 的地方直接替換為 100,減少變量訪問開銷
- 不可變分析:對于 final 引用類型(如 final String s),JIT 可以假設其引用不會變化,從而進行更激進的優化(如避免重復計算、減少鎖競爭等)
總結:final 底層的核心邏輯
修飾對象 | 底層實現核心 | 典型場景 |
類 | 字節碼標記 ACC_FINAL,禁止繼承 | String、Integer 等不可變類 |
方法 | 字節碼標記 ACC_FINAL ,禁止重寫 | 工具類中的固定邏輯方法(如 Objects.requireNonNull ) |
變量 | 編譯期禁止二次賦值 + 運行期內存屏障(多線程可見性) | 常量定義、多線程共享的不可變引用 |
final 的底層機制本質是 通過編譯期約束和運行期優化,保證 【不可變】或 【不可繼承 / 重寫】
同時為多線程環境提供了安全的內存語義,是 Java 中實現不可變性和線程安全的重要手段
?
final是如何保證多線程可見的
核心目標: 確保在多線程環境下,當一個對象被構造完成后,其他線程看到的該對象中被 final 修飾的字段的值,一定是構造方法中設置的那個值,不會看到未初始化的默認值(如 0, null 等)
關鍵前提條件:
- final 變量在構造方法中初始化完成
- 構造方法沒有“逸出”(this 引用未逃逸): 在構造方法執行結束之前,對象的 this 引用沒有被其他任何線程獲取到。這是安全發布的基礎
JVM 如何保證(底層依賴內存屏障):
為了達到這個目標,JVM 在編譯和運行時會插入特定的內存屏障指令
內存屏障可以簡單理解為阻止 CPU 或編譯器對指令進行重排序的柵欄,確保屏障前后的指令執行順序符合預期
- 寫屏障:
禁止對final字段賦值操作與構造方法結束后發生的所有的對final變量的寫入操作進行重排序
final字段的寫入一定發生在對象【引用發布】之前,也就是保證對象構建好后外部線程才能訪問
- 讀屏障 :
禁止對讀取final字段的操作與該讀取操作前的任何讀取操作進行重排序
強制要求讀取final字段時必須先去檢查最新的內存值,保證不丟失修改,保證讀取的數據值最新值
Happens-Before 規則的體現:
JMM 的 final 語義建立了一個 happens-before 關系:
- 構造方法中對 final 字段的賦值操作 happens-before 于構造方法的結束(return)
- 由于 StoreStore 屏障的保證,構造方法的結束 happens-before 于后續任何線程通過一個正確發布的引用對該對象的 final 字段的讀取操作
- 因此,構造方法中對 final 字段的賦值 happens-before 于任何線程對該 final 字段的讀取。 這就是為什么讀取線程一定能看到正確初始化的值
簡單來說:
JVM 通過在寫 final 字段后加寫屏障,在讀 final 字段前加讀屏障
配合構造方法不逸出的前提,巧妙地利用了內存屏障阻止了可能導致看到未初始化值的指令重排序
從而讓 final 字段成為多線程環境下一種安全、無需額外同步就能保證可見性的常量發布機制
這就是 final 字段 happens-before 語義的底層實現基礎
final底層-簡單總結
類:字節碼標記 ACC_FINAL,禁止繼承。編譯期和類加載期會檢查約束
方法:字節碼標記 ACC_FINAL,禁止重寫。編輯期和字節碼驗證期拒絕重寫
變量:底層通過字節碼標記 ACC_FINAL禁止二次賦值 + 運行期內存屏障保證多線程可見性
-基本類型變量:若在編譯期能確定賦值則會將其視為 【編譯期常量】,并可能觸發 常量折疊 優化
-引用類型變量:final 對引用類型的約束是 【引用不可變】,但對象本身的內容可以修改(如 list.add("a") 是允許的)
final的內存屏障如何保證多線程可見:
利用happen-before規則結合寫屏障和寫屏障
寫屏障:
禁止對final字段賦值操作與構造方法結束后發生的所有的對final變量的寫入操作進行重排序
final字段的寫入一定發生在對象【引用發布】之前,也就是保證對象構建好后外部線程才能訪問
讀屏障 :
禁止對讀取final字段的操作與該讀取操作前的任何讀取操作進行重排序
強制要求讀取final字段時必須先去檢查最新的內存值,保證不丟失修改,保證讀取的數據值最新值