08.3 - final 的内存语义
定位: final 在 JMM 中有特殊的可见性保证——确保构造函数中写入的 final 字段在其他线程读取时一定是初始化后的值。这是 JSR-133 (JDK 5) 针对构造函数安全发布的修复。 面试高频度: ⭐⭐⭐⭐ 考查方式: final 字段的可见性保证、构造函数逃逸问题、final 不等于不可变、final 与 JIT 优化的关系
一、这是什么?为什么需要它?
JSR-133 之前的 Bug:final 字段可能读到默认值
在 JDK 5 之前(JSR-133 之前),final 字段没有任何特殊的内存语义。这意味着:
JSR-133 之前的 JDK (1.4 及更早):
class FinalFieldProblem {
final int x;
static FinalFieldProblem instance;
public FinalFieldProblem() {
x = 42; // ① 构造函数中写入 final
}
static void create() {
instance = new FinalFieldProblem(); // ② 发布引用
}
static void reader() {
FinalFieldProblem obj = instance;
if (obj != null) {
int r = obj.x; // ③ 可能读到 0! 不是 42!
}
}
}根本原因:构造函数中的写操作和引用赋值可以被重排序。
无特殊语义时可能的执行顺序:
时间──→
┌────────────────────────────────────────────────────────────┐
│ Thread A (构造函数): │
│ allocate FinalFieldProblem (分配内存, 字段为默认值 0) │
│ astore instance ← 引用赋值被重排到这里! │
│ ... (构造函数体中, x=42 还没执行) │
│ │
│ Thread B (读): │
│ if (instance != null) → true │
│ int r = obj.x → 读到 0! (因为 x=42 还没执行) │
│ │
│ Thread A (继续执行构造函数): │
│ x = 42 ← 执行得太晚了 │
└────────────────────────────────────────────────────────────┘WHY JSR-133 修复:JMM 团队意识到,final 字段的语义应该是"构造函数完成 → final 字段一定已被初始化"。否则即使是最基本的安全发布模式(在构造函数中初始化,然后发布引用)也无法保证。
JSR-133 对 final 的两条规则
规则 1: 构造函数中的 final 字段写
─────────────────────────────────
final_field = value; ← 在构造函数中写入
[StoreStore 屏障] ← 在构造函数返回/引用赋值前插入
return; (或引用赋值)
→ 保证: 在对象引用对其他线程可见前, final 字段已经初始化完成
规则 2: final 字段的首次读
─────────────────────────────────
[LoadLoad 屏障] ← 在读 final 字段之前
int r = obj.finalField;
→ 保证: 读取 final 字段时, 一定看到构造函数中写入的值
(不会被后续的读操作重排序到前面)二、原理拆解
2.1 JSR-133 之后的 final 语义细节
关键约束:构造函数不泄漏 this
final 语义只在"构造函数不泄漏 this"时成立。如果构造函数中把 this 发布到外部,final 的保证就失效了:
模式 1: 安全发布 (final 语义生效)
┌────────────────────────────────────────────┐
│ class SafeObj { │
│ final int x; │
│ │
│ SafeObj(int val) { │
│ this.x = val; │
│ // 构造函数结束前没有暴露 this │
│ } │
│ } │
│ │
│ 方法返回后 → 其他线程看到 obj.x 一定是 val │
└────────────────────────────────────────────┘
模式 2: this 逃逸 (final 语义失效!)
┌────────────────────────────────────────────┐
│ class UnsafeObj { │
│ final int x; │
│ static UnsafeObj instance; │
│ │
│ UnsafeObj(int val) { │
│ this.x = val; │
│ instance = this; // ← this 逃逸! │
│ } │
│ } │
│ │
│ 其他线程可能看到 obj.x = 0! │
│ 因为 this 在构造函数完成前就发布了 │
└────────────────────────────────────────────┘WHY:如果 this 在构造函数中逃逸,JVM 无法确定"构造函数何时结束"——你让 this 在构造函数中可见,JVM 不能保证 final 字段在其可见之前已被写入。
final 字段和非 final 字段的区别
class Example {
final int a = 42; // final → JSR-133 保证
int b = 7; // 非 final → "尽可能"可见, 但不保证
static final int c = 100; // static final → 编译时常量 (类加载时确定)
Example() {
// a = 42 → 默认值初始化, 编译后直接写入
// b = 7 → 在构造函数中赋值
}
// 非 final 字段的可见性要依赖 synchronized/volatile 链:
// 如果对象是通过锁安全发布的, b 也对其他线程可见
// 如果是直接赋值给 volatile 字段 → 借助 volatile hb 链可见
void unsafePublish() {
// 直接暴露 this → a 和 b 都可能不可见
holder = this;
}
}2.2 final 不等于不可变 (Immutable)
这是最常被误解的地方。
final int x = 42; ← x 不能再赋值
final List<String> list = new ArrayList<>();
list = new ArrayList<>(); // 编译错误! Cannot assign a value to final variable
list.add("hello"); // 可以! 引用本身不变, 但内部状态变了
→ final 只保证引用不改变, 不保证引用的对象内部不变!真正的不可变对象 (Immutable Object) 需要:
┌────────────────────────────────────────────┐
│ ① 所有字段都是 final │
│ ② 类本身是 final (禁止子类覆盖行为) │
│ ③ 如果字段是引用类型, 确保不可变: │
│ - 不提供修改该引用的方法 │
│ - 引用的对象本身不可变 (如 String) │
│ - 构造时深度拷贝防御性 │
│ ④ 不泄漏 this 引用 │
└────────────────────────────────────────────┘
反例:
┌────────────────────────────────────────────┐
│ class NotImmutable { │
│ final int x; │
│ final List<String> items; // final 引用 │
│ │
│ NotImmutable(List<String> items) { │
│ this.items = items; // 直接引用! │
│ this.x = items.size(); │
│ } │
│ │
│ List<String> getItems() { │
│ return items; // 暴露了内部引用! │
│ } │
│ } │
│ │
│ // 外部可以修改: │
│ list.add("hacked"); // 内部状态变了! │
│ obj.items == list → true (同一个引用) │
└────────────────────────────────────────────┘2.3 final 与 JIT 优化的协同
Final 字段帮助 JIT 做常量传播和死代码消除:
class Config {
final int timeout = 5000; // 编译期已知
final int maxRetries = 3;
}
// JIT 编译后 (假设对象不逃逸):
for (int i = 0; i < 3; i++) {
connect(timeout: 5000); // 常量直接展开
}
// timeout 和 maxRetries 被内联为立即数
// 不需要从对象头读取!
// vs 非 final:
for (int i = 0; i < config.maxRetries; i++) {
connect(config.timeout); // 每次都要从堆上读取
// 如果 C2 证明 config 不逃逸, 也可以优化
// 但 final 让编译器自信地做常量折叠
}Final + 逃逸分析:对象不逃逸且字段都是 final → 标量替换后字段直接变成立即数,连对象本身都消失了。
标量替换后:
// Config 对象完全消失!
int timeout$local = 5000;
int maxRetries$local = 3;
for (int i = 0; i < 3; i++) {
connect(timeout$local); // 更进一步: connect(5000)
}2.4 static final 的特殊性
class Constants {
static final int PORT = 8080; // 编译期常量
static final String NAME = "server";
// 类加载时在 <clinit> 中初始化
// 所有线程在类初始化完成后看到的是同一份值
// 非编译期常量 (需要运行时决定):
static final long START_TIME = System.currentTimeMillis();
// 也在 <clinit> 中初始化, 但依赖 JVM 运行状态
}编译期常量 (static final + 字面量):Java 编译器会直接"内联"到使用处。反编译后,引用 Constants.PORT 的地方变成了 bipush 8080。
三、图解全景
3.1 JSR-133 前后对比
JSR-133 之前 (JDK ≤ 1.4):
┌────────────────────────────────────────────────────────────────────┐
│ Thread A (构造函数): │
│ allocate │
│ astore instance ← 引用逃逸! 构造函数还没执行完 │
│ x = 42 ← 太晚了! │
│ │
│ Thread B (读): │
│ read instance → 非 null │
│ read instance.x → 0! (默认值) │
│ │
│ 结论: final 和非 final 一样, 没有可见性保证 │
└────────────────────────────────────────────────────────────────────┘
JSR-133 之后 (JDK 5+):
┌────────────────────────────────────────────────────────────────────┐
│ Thread A (构造函数): │
│ allocate │
│ x = 42 ← 先写入 final 字段 │
│ [StoreStore] ← 编译器插入的屏障: 确保 x=42 写入后才发布引用 │
│ astore instance ← 此时 x=42 已对其他线程可见 │
│ │
│ Thread B (读): │
│ read instance → 非 null │
│ read instance.x → 42! (一定看到构造函数写入的值) │
│ │
│ 结论: final 字段有可见性保证 (通过 StoreStore 屏障) │
└────────────────────────────────────────────────────────────────────┘3.2 构造函数 this 逃逸场景
场景: 在构造函数中注册事件监听器
┌────────────────────────────────────────────────────────────────────┐
│ class EventSource { │
│ final String name; │
│ static EventSource latest; │
│ │
│ EventSource(String name) { │
│ this.name = name; │
│ latest = this; ← (1) this 逃逸! │
│ GlobalRegistry.register(this); ← (2) 或者这种形式 │
│ new Thread(() -> { │
│ use(this.name); ← (3) 内部类捕获 this │
│ }).start(); │
│ } │
│ } │
│ │
│ 问题: any of (1)(2)(3) 发生后, 其他线程可能通过 latest / │
│ registry / thread 引用到这个对象——而构造函数还没结束! │
│ final 语义不保证看到 name 的值! │
│ (因为 StoreStore 屏障只在"构造函数结束"时生效, │
│ 逃逸后构造函数不算"结束") │
└────────────────────────────────────────────────────────────────────┘3.3 Final 字段的引用类型陷阱
class Order {
final List<String> items; // final 引用
Order(List<String> items) {
this.items = items; // 直接持有外部引用
}
}
// 危险用法:
List<String> mutable = new ArrayList<>();
Order order = new Order(mutable);
mutable.add("illegal"); // 通过外部引用修改了 order 的内部状态!
order.items.get(0) → "illegal" // 虽然是 final, 但对象变了!
// 安全做法 (防御性拷贝):
Order(List<String> items) {
this.items = new ArrayList<>(items); // 拷贝一份
}
// 或者用不可变集合:
Order(List<String> items) {
this.items = Collections.unmodifiableList(new ArrayList<>(items));
}四、实战验证
验证 1: final 字段的可见性
java
public class FinalVisibility {
private static FinalObj instance;
static class FinalObj {
final int x; // final → 保证可见
int y; // 非 final → 不一定可见
FinalObj() {
x = 42;
y = 42;
}
}
// 不安全发布
static void publish() {
instance = new FinalObj();
}
public static void main(String[] args) throws Exception {
for (int round = 0; round < 10000; round++) {
instance = null;
Thread writer = new Thread(FinalVisibility::publish);
Thread reader = new Thread(() -> {
FinalObj obj = instance;
if (obj != null) {
if (obj.x != 42) {
System.out.println("FINAL BUG: x = " + obj.x);
}
if (obj.y != 42) {
System.out.println("NON-FINAL BUG (expected): y = " + obj.y);
}
}
});
writer.start();
reader.start();
writer.join();
reader.join();
}
System.out.println("Done. x should always be 42 (JSR-133 guarantee)");
}
}验证 2: this 逃逸问题
java
public class ThisEscape {
static ThisEscape instance;
final int value;
public ThisEscape(int val) {
this.value = val;
instance = this; // this 逃逸! final 语义失效
// 此时其他线程通过 instance 引用可能看到 value=0
}
public static void main(String[] args) throws Exception {
for (int round = 0; round < 100000; round++) {
instance = null;
Thread writer = new Thread(() -> new ThisEscape(42));
Thread reader = new Thread(() -> {
ThisEscape obj = instance;
if (obj != null && obj.value != 42) {
System.out.println("ESCAPE BUG: value = " + obj.value);
}
});
writer.start();
reader.start();
writer.join();
reader.join();
}
System.out.println("Done. this escape may show stale values.");
}
}验证 3: final 引用类型的可修改性
java
public class FinalReferenceMutability {
public static void main(String[] args) {
final StringBuilder sb = new StringBuilder("Hello");
// sb = new StringBuilder("World"); // 编译错误! Cannot assign
sb.append(" World"); // 可以修改对象内部状态
System.out.println(sb); // "Hello World"
// final 阻止的是引用本身改变, 不是对象状态改变
// 语义对比:
// final int x = 42; → x 不能再赋值 (值和引用都不可变)
// final StringBuilder s → s 不能再指向其他对象
// 但 s 指向的对象内部状态可变
}
}五、面试视角
| 追问 | 答案要点 |
|---|---|
| final 字段在 JMM 中有什么特殊语义? | JSR-133 (JDK 5) 之后,final 字段有特殊的可见性保证:构造函数中写入 final 字段后,编译器会在构造函数返回前插入 StoreStore 屏障。其他线程看到对象引用时,final 字段一定已被构造函数初始化过。 |
| final 能保证对象不可变吗? | 不能。final 只保证引用本身不能重新赋值,不保证引用的对象内部状态不变。要构造真正的不可变对象,还需要:所有字段 final + 类 final + 引用类型防御性拷贝 + 不泄漏内部引用 + 不泄漏 this。 |
| 构造函数中泄漏 this 有什么问题? | this 泄漏意味着对象在构造函数完成前就被发布。此时 final 字段的 JSR-133 保证失效——其他线程可能看到默认值(0/null)。因为 JMM 无法确定"构造函数结束"的边界。 |
| JSR-133 对 final 做了什么修复? | 两条规则:(1) 构造函数中对 final 字段的写 → 在构造函数返回前插入 StoreStore 屏障,确保 final 字段的写入在引用发布前完成。(2) 读 final 字段 → 插入 LoadLoad 屏障,确保读到的是构造函数中写入的值。 |
| static final 和实例 final 有什么区别? | static final 在类加载的 <clinit> 阶段初始化,类加载完成后所有线程可见(类加载本身有锁保护)。实例 final 在构造函数中初始化,依赖 JSR-133 的 StoreStore 屏障保证可见性。 |
| final 字段如何辅助 JIT 优化? | (1) 常量折叠:final 字段值可被内联为立即数。(2) 标量替换:对象不逃逸 + final 字段 → 字段直接变成局部变量/立即数。(3) 死代码消除:基于 final 值判断的分支可被消除。(4) 循环优化:final 边界值可在编译期确定。 |
为什么 String 是不可变的? | String 类被声明为 final,所有字段都是 private final(value char[] 是 final 引用),不提供修改内部状态的 public 方法,不泄漏内部引用。这是 final 语义 + 类设计共同保证的。 |
| 反序列化会破坏 final 吗? | 是的。反序列化不通过构造函数创建对象(使用 Unsafe.allocateInstance 或 ObjectInputStream),final 字段的 JSR-133 保证不适用。反序列化可能绕过 final 约束——这就是为什么需要 readResolve() 或 readObject() 来控制反序列化行为。 |
📚 相关链接
- **JMM与happens-before** — JMM 的核心规则,final 是 hb 规则 8(对象终结规则)的基础
- **volatile的内存语义** — volatile 的 StoreStore 屏障和 final 的 StoreStore 屏障类似
- **对象内存布局(MarkWord与Klass与实例数据)** — 对象头和实例数据布局
- **方法内联和逃逸分析** — 标量替换如何受益于 final
- **锁消除与标量替换** — 标量替换的条件与 final 协同
- ← 返回 **JMM索引**