微信公众号:Vegout 如有问题或建议,请公众号留言
final修饰的类不能被继承,修饰的方法不能被重写,修饰的变量不能被二次赋值,总之,final就是最终的意思,保证了不变性。除了对不变性的保障,对有序性final也做出了他的贡献。
在线程安全中,有三大特性需要保障——原子性,可见性,有序性。而final对于有序性拥有特殊的语义。当一个类的一个变量声明为final类型,那么这个类初始化完成时,这个final变量必定完成了初始化。这个我们可以和普通变量做一个对比
public class FinalTest {
final int a;
int b;
static FinalTest instance;
private FinalTest(){
a=1;
b=2;
}
public void FinalTest init(){
instance = new FinalTest();
}
public static void print(){
FinalTest finalTest = instance;
if(finalTest!=null)
System.out.println("a="+finalTest.a+" "+"b="+finalTest.b);
}
}
假设我们有两个线程,线程A执行init()方法,线程B执行print()方法,按照正常情况,打印出来的a=1,b=2。但是也有可能打印出a=1,b=0,为什么呢?init()函数调用构造函数进行了对象的初始化,当构造函数返回的时候,a和b一定完成了赋值吗?这是不一定的,因为编译器和处理器对代码的执行存在重排序的可能,普通变量的赋值可能被重排序到构造函数返回之后进行,所以打印出的b有等于0的可能,而final修饰的变量可以确保不会被重排序到构造函数之外,因此,对与final修饰的变量,确保了初始化的安全性。如果final修饰的是引用类型,那么相当于这个引用(地址)不可变,但引用指向的对象是可变的。这种情况下,对与有序性有如下保证
也就是说我们在构造函数中对于final修饰的引用型成员变量指向的对象的赋值可以在构造函数返回之前完成,同样,对于普通引用变量,享受不到这个保证。例如上边的例子修改为
public class FinalTest {
final int[] a = new int[10];
int b;
static FinalTest instance;
private FinalTest(){
a[0] = 1;
a[1] = 2;
b=2;
}
public void FinalTest init(){
instance = new FinalTest();
}
public static void print(){
FinalTest finalTest = instance;
if(finalTest!=null)
System.out.println("a="+finalTest.a+" "+"b="+finalTest.b);
}
}
那么构造函数中的a[0]和a[1]必定是完成了初始化的,而不会被重排序到构造函数返回之后进行,这个也是final来保证的。 final保证了这么多,其实底层采用的就是内存屏障,当编译器检测到final类型的变量时,初始化时它对应的操作就不会进行以上的重排序,并且在合适的位置插入内存屏障,告诉处理器也不要进行重排序。
以上说的对与final类型成员变量的初始化的保证还有一个前提条件——被构造对象不能在构造函数中逸出。如果上边构造函数改为
private FinalTest(){
a=1;
b=2;
instance = this;
}
this在构造函数还没有完成之前就对其他线程可见,这是一种危险的操作,使得对象不能完成安全的初始化,可能在构造函数没有返回之前,B线程就调用了print方法,并且通过逸出的instance访问没有进行初始化的成员变量,从而对与final的有序性语义也就难以进行保证了。