# Java 中的 final 关键字是否能保证变量的可见性?

5 min read
Table of Contents

Java 中的 final 关键字是否能保证变量的可见性?

【核心定义】
final 关键字本身不能保证变量的可见性,它仅保证引用或基本类型值的不可变性,但通过 JMM(Java 内存模型)的特殊规则,正确构造的 final 字段在对象初始化完成后对其他线程具有可见性。

【关键要点】

  1. final 字段的“冻结”语义

    • 在对象构造期间,final 字段的写入与后续的对象引用赋值之间存在“happens-before”关系。
    • 一旦对象构造完成(引用被发布),其他线程看到的 final 字段值一定是构造器内写入的最终值,无需同步。
  2. 非 final 字段无此保证

    • 普通字段的写入可能因指令重排序导致其他线程看到未初始化的值(部分初始化问题)。
    • 示例:线程 A 构造对象时,若未正确同步,线程 B 可能看到 int 型普通字段为默认值 0,而 final 字段必为构造器设定的值。
  3. final 引用与对象内部状态

    • final 仅保证引用本身不变,但引用指向的对象内部状态(如 final Map 的内容)仍可能因非同步访问出现可见性问题。
    • 需额外同步(如 volatile、锁)来保证可变对象内部状态的可见性。

【深度推导/细节】
JMM 对 final 的特殊规则(以 OpenJDK 实现为例)

  • 编译器和 CPU 重排序限制:在构造器内,final 字段的写入不能重排序到构造器之外。
  • 内存屏障插入:JVM 会在 final 字段写入后、对象引用发布前插入 StoreStore 屏障(如 x86 的 mfence),确保 final 值对其他线程立即可见。
  • 安全发布的必要条件:对象引用必须通过“安全发布”方式(如存入 volatile 变量、AtomicReference、synchronized 块)传递,否则可能因引用本身可见性问题导致 final 字段可见性失效。

临界场景分析

class Example {
final int x;
int y;
Example(int val) {
y = 1; // 普通字段写入可能重排序
x = val; // final 写入不会重排序到构造器外
}
}
  • 若线程 A 构造 Example(5) 后未同步发布,线程 B 可能看到 y == 0(默认值),但 x 必定为 5。

【关联/对比】

  • final vs volatile
    • volatile 保证任意线程的读写可见性及禁止重排序;final 仅保证构造完成后的一次可见,后续修改不适用。
    • 若字段需构造后修改,应使用 volatile 或原子类。
  • final 与不可变对象
    • 完全由 final 字段构成且字段类型不可变(如 String、基本类型)的对象是线程安全的;若包含可变引用(如 final List<String>),需通过防御性拷贝保证安全。

『面试官追问』

  1. final 字段在反射修改后是否仍保证可见性?
    • 否。反射 setAccessible(true) 可修改 final 字段,但会破坏 JMM 保证,可能导致不可预测行为。
  2. 静态 final 字段(常量)的可见性如何?
    • 静态 final 字段在类初始化阶段(<clinit>)赋值,遵循类加载的同步规则,对其他线程立即可见。
  3. final 字段在反序列化时的可见性?
    • 反序列化通过反射设置字段,不触发构造器内存屏障,需额外同步保证可见性。

【总结】
final 通过 JMM 规则在对象正确构造并安全发布的前提下,提供初始化阶段的可见性保证,但并非通用可见性解决方案。高并发场景中,共享可变状态仍需依赖 volatile、锁或原子变量。

My avatar

文章看完了,不打算去别的篇章转转吗?👀 感谢你的耐心阅读!如果你有问题想探讨,或者发现了文中的 Bug,请务必通过页脚的链接联系我


More Posts

Comments