Java:final的作用和原理介绍
一. final 的作用
class A {private final Object obj; // #1public A(){this.obj = new Object(); // $1}public A(Object obj){this.obj = obj; // $2}public void doTest(){Object o1 = new Object();final Object o2 = o1; // #2Runnable run = new Runnable() {public void run() {System.out.println(o2);}};o1 = new Object();Thread t = new Thread(run);t.start();}
}
1. 语义层面
类中由final修饰的属性#1,方法中由final修饰的变量#2,表示:引用不可变(不能重新赋值)。如果尝试重新赋值,编译器会报错。
2. 多线程可见性
final 的可见性保证仅限于构造函数内的赋值$1、$2(安全发布模式)。
在普通代码块中#2,final 不提供内存屏障,因此:其他线程可能看到 o2 的旧值(尽管实际场景中 HotSpot JVM 可能会优化,但规范不保证)。
如果 o2 的赋值在构造函数内,JVM 会保证可见性。
3. 匿名内部类访问外部变量
在 Java 中,匿名内部类(如 Runnable)访问外部局部变量时,该变量必须是 final 或等效不可变(Java 8 后允许 effectively final)。
代码中,如果移除 final:
- 但 o2 后续不修改(effectively final),Java 8+ 仍允许编译。
- 但如果 o2 被修改(如 o2 = new Object();),则必须用 final 修饰。
二. final 的内存语义
根据 JLS §17.5(Java 语言规范):
final 字段的写入必须在对象的构造函数完成之前完成,才能保证其他线程看到正确的初始值。
final关键字的可见性保证,仅限于对象的构造阶段(即构造函数内的赋值),而不适用于普通赋值。
具体规则如下:
1. final 在构造函数内的赋值(安全发布)
如果一个 final 字段在 构造函数内 被赋值,JVM 会确保:
- 该字段的写入不会被重排序到构造函数之外(防止其他线程看到未初始化的值)。
- 当构造函数执行完毕时,final 字段的值对所有线程可见(类似于 volatile 的初始化安全)。
java示例:
class MyClass {final Object o2;MyClass(Object o1) {this.o2 = o1; // 构造函数内赋值,保证可见性}
}
安全发布的两种方式:
(1) 构造函数内赋值(显式初始化)
class A {final Object obj;A() {this.obj = new Object(); // 安全发布}
}
(2) 声明时直接赋值(隐式初始化)
class A {final Object obj = new Object(); // 是否安全发布?
}
final 字段在声明时直接赋值,仍然属于安全发布,因为:编译器会将声明时的赋值逻辑放到构造函数中(字节码层面等同于构造函数内初始化)。
上述代码编译后等价于:
class A {final Object obj;A() {this.obj = new Object(); // 编译器自动插入}
}
JVM 会保证这种写法的可见性,其他线程读取 A.obj 时不会看到 null 或未初始化的状态。指令重排序被禁止:JVM 会确保 final 字段的初始化操作不会被重排序到构造函数之外。
2. final 在普通代码块的赋值(无内存屏障)
如果 final 字段在 非构造阶段(如普通方法或代码块)赋值,它不会提供任何内存屏障或可见性保证。
Object o1 = new Object();
final Object o2 = o1; // 普通赋值,无内存屏障
这里的 final 仅表示 o2 引用不可变(不能重新赋值),但 不保证其他线程能立即看到 o2 的值。
Object o1 = new Object();
final Object o2 = o1;Runnable run = new Runnable() {public void run() {System.out.println(o2);}
};
o1 = new Object();Thread t = new Thread(run);
t.start();
在这段代码中,final 关键字用于声明局部变量 o2 是不可变的。这意味着一旦 o2 被赋值后,就不能再更改它的引用。
具体来说,在 Java 中,匿名内部类(如这里的 Runnable 实现)不能访问外部作用域中的非 final 局部变量,这是因为这些变量在方法执行结束后可能不再存在,而匿名内部类的对象可能会比方法活得更久。因此,Java 强制要求对外部局部变量的引用必须是 final 或 effectively final(即实际上不会被修改)。
如果去掉 final 关键字,编译器会报错,因为 o2 在匿名内部类中被使用,但它不是 final 的。