# Java 中的 final 关键字是否能保证变量的可见性?
5 min read
Table of Contents
Java 中的 final 关键字是否能保证变量的可见性?
【核心定义】
final 关键字本身不能保证变量的可见性,它仅保证引用或基本类型值的不可变性,但通过 JMM(Java 内存模型)的特殊规则,正确构造的 final 字段在对象初始化完成后对其他线程具有可见性。
【关键要点】
-
final 字段的“冻结”语义
- 在对象构造期间,final 字段的写入与后续的对象引用赋值之间存在“happens-before”关系。
- 一旦对象构造完成(引用被发布),其他线程看到的 final 字段值一定是构造器内写入的最终值,无需同步。
-
非 final 字段无此保证
- 普通字段的写入可能因指令重排序导致其他线程看到未初始化的值(部分初始化问题)。
- 示例:线程 A 构造对象时,若未正确同步,线程 B 可能看到
int型普通字段为默认值 0,而 final 字段必为构造器设定的值。
-
final 引用与对象内部状态
- final 仅保证引用本身不变,但引用指向的对象内部状态(如
final Map的内容)仍可能因非同步访问出现可见性问题。 - 需额外同步(如
volatile、锁)来保证可变对象内部状态的可见性。
- final 仅保证引用本身不变,但引用指向的对象内部状态(如
【深度推导/细节】
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>),需通过防御性拷贝保证安全。
- 完全由 final 字段构成且字段类型不可变(如
『面试官追问』
- final 字段在反射修改后是否仍保证可见性?
- 否。反射
setAccessible(true)可修改 final 字段,但会破坏 JMM 保证,可能导致不可预测行为。
- 否。反射
- 静态 final 字段(常量)的可见性如何?
- 静态 final 字段在类初始化阶段(
<clinit>)赋值,遵循类加载的同步规则,对其他线程立即可见。
- 静态 final 字段在类初始化阶段(
- final 字段在反序列化时的可见性?
- 反序列化通过反射设置字段,不触发构造器内存屏障,需额外同步保证可见性。
【总结】
final 通过 JMM 规则在对象正确构造并安全发布的前提下,提供初始化阶段的可见性保证,但并非通用可见性解决方案。高并发场景中,共享可变状态仍需依赖 volatile、锁或原子变量。