final关键字
# final关键字
final关键字:
- 用来修饰一个引用:
- 如果引用为基本数据类型,则该引用为常量,该值无法修改;
- 如果引用为引用数据类型,比如对象、数组,则该对象、数组本身可以修改,但指向该对象或数组的地址的引用不能修改。
- 如果引用是类的成员变量,则必须当场赋值,否则编译会报错。
- 用来修饰一个方法:当使用final修饰方法时,这个方法将成为最终方法,无法被子类重写。但是,该方法仍然可以被继承。使用final方法的原因主要有两个:
- 把方法锁定,以防止继承类对其进行更改
- 效率,在早期的Java版本中,会将final方法转为内嵌调用。但若方法过于庞大,可能在性能上不会有多大提升。因此在最近版本中,不需要final方法进行这些优化了。
- 用来修饰类:当用final修饰类时,该类成为最终类,无法被继承,该类就不能被其他类所继承;简称为“断子绝孙类”。当我们需要让一个类永远不被继承,此时就可以用final修饰,但要注意:final类中所有的成员方法都会隐式的定义为final方法。
# 构造方法溢出问题
考虑下面的代码:
public class MyClass {
private int num1;
private int num2;
private static MyClass myClass;
public MyClass() {
num1 = 1;
num2 = 2;
}
/**
* 线程A先执行write()
*/
public static void write() {
myClass = new MyClass();
}
/**
* 线程B接着执行write()
*/
public static void read() {
if (myClass != null) {
int num3 = myClass.num1;
int num4 = myClass.num2;
}
}
}
num3和num4的值是否一定是1和2?
num3、num4不见得一定等于1,2,和DCL的例子类似,也就是构造方法溢出问题。myClass = new MyClass()
这行代码,分解成三个操作:
- 分配一块内存;
- 在内存上初始化
i=1,j=2
; - 把myClass指向这块内存。
操作2和操作3可能重排序,因此线程B可能看到未正确初始化的值。对于构造方法溢出,就是一个对象的构造并不是“原子的”,当一个线程正在构造对象时,另外一个线程却可以读到未构造好的“一半对象”。
# final的happen-before语义
要解决这个问题,有多种办法:
- 办法1:给num1,num2加上
volatile
关键字。 - 办法2:为
read/write
方法都加上synchronized
关键字。
如果num1,num2只需要初始化一次,还可以使用final
关键字。之所以能解决问题,是因为同volatile
一样,final关键字也有相应的happen-before语义:
- 对
final
域的写(构造方法内部)happen-before于后续对final域所在对象的读。 - 对
final
域所在对象的读,happen-before于后续对final域的读。
通过这种happen-before语义的限定,保证了final域的赋值,一定在构造方法之前完成,不会出现另外一个线程读取到了对象,但对象里面的变量却还没有初始化的情形,避免出现构造方法溢出的问题。
# happen-before规则总结
- 单线程中的每个操作,happen-before于该线程中任意后续操作。
- 对volatile变量的写,happen-before于后续对这个变量的读。
- 对synchronized的解锁,happen-before于后续对这个锁的加锁。
- 对final变量的写,happen-before于final域对象的读,happen-before于后续对final变量的读。
四个基本规则再加上happen-before的传递性,就构成JMM对开发者的整个承诺。在这个承诺以外的部分,程序都可能被重排序,都需要开发者小心地处理内存可见性问题。
上次更新: 5/30/2023, 12:05:21 AM