详解java Final关键字


        本文主要详解java Final关键字,更多Java技术知识,请登陆疯狂软件教育官网。
  常用用法

  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字段就被冻结了。

你可能感兴趣的:(java,关键字,寄存器,编译器)