final修饰符不可变的底层

final修饰符的底层原理

在 Java 中,final 修饰符的底层实现涉及 编译器优化JVM 字节码层面的约束

其核心目标是保证被修饰元素的【不可变性】或 【不可重写 / 继承性】


一、final 修饰类:禁止继承的底层约束

当一个类被 final 修饰时,例如 String 、Integer

JVM 在字节码层面会通过 访问标志(access flags) 标记该类为 ACC_FINAL

  • 编译器在编译时会检查:如果子类试图继承被 final 修饰的类,会直接抛出编译错误
    【无法继承最终类】
  • JVM 在类加载阶段也会验证这一约束,确保没有非法继承行为

本质

  1. 通过字节码标记禁止继承
  2. 属于 编译期和类加载期 的静态约束,不涉及运行时的特殊处理

二、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 等)


关键前提条件:

  1. final 变量在构造方法中初始化完成
  2. 构造方法没有"逸出"(this 引用未逃逸): 在构造方法执行结束之前,对象的 this 引用没有被其他任何线程获取到。这是安全发布的基础

JVM 如何保证(底层依赖内存屏障):

为了达到这个目标,JVM 在编译和运行时会插入特定的内存屏障指令

内存屏障可以简单理解为阻止 CPU 或编译器对指令进行重排序的栅栏,确保屏障前后的指令执行顺序符合预期


  1. 写屏障:

禁止对final字段赋值操作与构造方法结束后发生的所有的对final变量的写入操作进行重排序

final字段的写入一定发生在对象【引用发布】之前,也就是保证对象构建好后外部线程才能访问

  1. 读屏障 :

禁止对读取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字段时必须先去检查最新的内存值,保证不丢失修改,保证读取的数据值最新值

相关推荐
苏三说技术1 小时前
Claude Code从失控到起飞,只用了这些技巧
后端
长栎2 小时前
写 for 循环写了十年,你却从没用过迭代器模式最狠的那一面
后端
LiaCode2 小时前
Redis 在生产项目的使用
前端·后端
用户559822481222 小时前
Docker Compose Down 导致容器数据误删——ext4 日志恢复全记录
后端
LiaCode2 小时前
一天学完 redis 的爽翻版核心知识总结
前端·后端
大刚测试开发实战2 小时前
如何内网穿透访问本地私有化部署的TestHub
前端·后端·github
xiaodaoluanzha3 小时前
迄今為止,最簡單的編程語言 Nolang
前端·后端
Csvn3 小时前
Docker 容器管理入门 — 从镜像到容器编排
后端
用户762352425913 小时前
ShardingJDBC
后端
行者全栈架构师3 小时前
IDEA 中 Maven 项目的 15 个红色报错快速解决方法
java·后端