常用用法
java中final关键字大家经常使用。final可以用于声明字段、方法和类。final声明字段时,若为基本类型,表示该变量值初始化后不再改变;若为引用类型,则表示引用不可变,但引用所指向的对象是可以改变的。final声明方法时表示方法不可覆写(常用来限制子类不可以改写父类中方法)。final声明类时,表示类不可继承,如String类就是final的,你不能继承它。
final字段的详细语义与普通字段稍有不同。尤其是,编译器有很大的自由,能将对final字段的读操作移到同步屏障之外,然后调用任意或未知的方法。同样,也允许编译器将final字段的值保存到寄存器,在非final字段需要重新加载的那些地方,final字段无需重新加载。另外,将对象声明为不可变的,则可实现并发访问,即final可提供一种非同步状态下 轻量级的 线程安全方法。
详细语义
final字段语义有以下几个目标:
1、final字段的值不会变化。编译器不应该因为获得了一个锁,读取了一个volatile变量或调用了一个未知方法,而重新加载一个final字段。
2、一个对象,仅包含final字段且在构建期间没有对其他线程可见,应当视作不可变的,即使这类对象的引用在线程间传递时存在数据争用。
3、将字段 f 设为final,在读取 f 时应当利用最小的编译器/架构代价。
4、该语义必须支持诸如反序列化等场景,在这种情况下,一个对象的final字段会在该对象构建完成后改变。
解释第二条语义之前,先说一下什么叫对象
逸出。当某个不该被发布的对象被发布时即为逸出。如下示例:
public class FinalReferenceEscapeExample {
final int i;
static FinalReferenceEscapeExample obj;
public FinalReferenceEscapeExample () {
i = 1; //1写final域
obj = this; //2 this引用在此“逸出”,对象尚未构造完成时外部便可访问,此时的final字段是不安全的。
}
public static void writer() {
new FinalReferenceEscapeExample ();
}
public static void reader {
if (obj != null) { //3
int temp = obj.i; //4
}
}
}
第二条中的构建期间没有对其它线程可见 一般即指正确的发布对象,也就是发布期间对象不可逸出。
关于第3条,先说说对于final字段编译器和处理器应该遵守的重排序规则
a、读final字段的重排序规则:
在一个线程中,初次读对象引用与初次读该对象包含的final域,JMM禁止处理器重排序这两个操作(注意,这个规则仅仅针对处理器)。编译器会在读final域操作的前面插入一个LoadLoad屏障。
初次读对象引用与初次读该对象包含的final域,这两个操作之间存在间接依赖关系。由于编译器遵守间接依赖关系,因此编译器不会重排序这两个操作。大多数处理器也会遵守间接依赖,大多数处理器也不会重排序这两个操作。但有少数处理器允许对存在间接依赖关系的操作做重排序(比如alpha处理器),这个规则就是专门用来针对这种处理器。
b、写final字段重排序规则:
JMM禁止编译器把final域的写重排序到构造函数之外。
编译器会在final域的写之后,构造函数return之前,插入一个StoreStore屏障。这个屏障禁止处理器把final域的写重排序到构造函数之外。
写final字段的重排序规则可以确保:在对象引用为任意线程可见之前,对象的final字段已经被正确初始化过了,而普通字段不具有这个保障。
如果final字段是引用类型,则写final字段的重排序规则增加了以下约束:
在构造函数内对一个final引用的对象的成员域的写入,与随后在构造函数外把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。
public class FinalReferenceExample {
final int[] intArray; //final是引用类型
static FinalReferenceExample obj;
public FinalReferenceExample () { //构造函数
intArray = new int[1]; //1
intArray[0] = 1; //2
}
public static void writerOne () { //写线程A执行
obj = new FinalReferenceExample (); //3
}
public static void writerTwo () { //写线程B执行
obj.intArray[0] = 2; //4
}
public static void reader () { //读线程C执行
if (obj != null) { //5
int temp1 = obj.intArray[0]; //6
}
}
}
对上面的示例程序,我们假设首先线程A执行writerOne()方法,执行完后线程B执行writerTwo()方法,执行完后线程C执行reader ()。JMM可以确保读线程C至少能看到写线程A在构造函数中对final引用对象的成员域的写入。即C至少能看到数组下标0的值为1。而写线程B对数组元素的写入,读线程C可能看的到,也可能看不到。JMM不保证线程B的写入对读线程C可见,因为写线程B和读线程C之间存在数据竞争,此时的执行结果不可预知。
上面我们提到,写final域的重排序规则会要求译编器在final域的写之后,构造函数return之前,插入一个StoreStore障屏。读final域的重排序规则要求编译器在读final域的操作前面插入一个LoadLoad屏障。由于x86处理器不会对写-写操作做重排序,所以在x86处理器中,写final域需要的StoreStore障屏会被省略掉。同样,由于x86处理器不会对存在间接依赖关系的操作做重排序,所以在x86处理器中,读final域需要的LoadLoad屏障也会被省略掉。也就是说在x86处理器中,final域的读/写不会插入任何内存屏障!
再说说上面的第4条,在有些时候(如反序列化),系统需要在对象创建完之后修改对象的final字段值。final字段可以通过反射和其他依赖于实现的方式来修改。这种情况唯一存在合理语义的场景是,对象被创建,然后其final字段被更新。在该对象的final字段的所有更新完成之前,该对象不应该对其他线程可见,且final字段也不应该被读取。在设置final的构造器结束时,以及通过反射或其他机制一修改完final字段,final字段就被冻结了。
即使如此,还是会有一些并发问题。如果final字段在字段声明中被初始化成一个编译时常量(ConstantValue。
注:如果同时使用static和final来修饰一个变量,且这个变量的类型是基本类型或者String的话,就生成ConstantValue来初始化。在类加载后,连接阶段中的准备阶段直接对其初始化,不会先初始化为0。其中,连接包含 验证、准备(为类变量分配内存,并设置类变量初始值0)、解析三个阶段))对该final字段的修改将不可见,因为使用该final字段的地方都在编译时被替换成了编译时常量。所以静态final字段仅
能在类初始化时赋值,不能通过反射修改。
另一个问题是,该语义是被设计来允许final字段的激进优化的。在一个线程内,允许将对final字段的读取与可能会通过反射改变final字段的方法调用进行重排序。
为了避免这种问题,有些实现可能会提供一种方式,在final字段安全上下文(final field safe context)中执行一块代码。如果对象是在final字段安全上下文里创建的,final字段的读取将不会与发生在final字段安全上下文中的final字段修改进行重排序。final字段安全上下文还有其他保护作用。如果一个线程看到了某个未正确发布的对象引用(允许该线程看到final字段的默认值),那么,在final字段安全上下文里,在读取该对象的某个正确发布的引用后,那些使用未正确发布引用的读操作也将确保能看到final字段正确的值。
适合使用final字段安全上下文的一个地方是在executor或线程池中。通过在单独的final字段安全上下文里执行各个Runnable,executor可以保证当某个Runnable未正确访问某个对象o时,不会导致该executor处理的其他Runnable失去final字段保证。实现中,一般来讲,编译器不应该将对final字段的访问移入或移出final字段安全上下文。但可以在这样的上下文执行的周围移动,只要对象不是在该上下文里创建的。
以上就是全部内容,主要参考了 JSR133中文版 和 深入理解java内存模型,可以说大部分是摘抄啦,主要是自己总结一下。非常感谢这两篇文档的作者,使我获益良多。