java Final关键字语义剖析

常用用法

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内存模型,可以说大部分是摘抄啦,主要是自己总结一下。非常感谢这两篇文档的作者,使我获益良多。

猜你喜欢

转载自blog.csdn.net/quanzhongzhao/article/details/45694711