final关键字及其内存语义

 

  1、 final  

  学习的要义:多问问出现的背景,可以解决什么问题,如何使用,对比别的方案有什么优点,是否有改进的地方?
html

 1.概述

    final关键字可以告诉编译器一块数据是恒定不变的,thinking in java 中提到的不想被改变的两种理由:设计和效率。java

 2.用法 :熊猫人都知道.....

 

    2.1 修饰变量

    做用:表示该变量不能够被再次赋值修改;编程

 变量主要分为基本数据类型和引用;并发

    ①若是修饰的是基本数据类型,就表示该变量的值永远不会改变,若是你试图改变他,例以下面的代码则会出现编译器警告:oracle

  Cannot assign a value to final variable ide

    private final int par = 0;
public void modifyFinalTest() { par = 4; }

 另外须要注意的是java容许用final修饰某个未进行初始化复制的变量,这叫作“空白final”,可是编译器必定可以确保空白final在使用前被初始化,否则报错给你看   函数

例以下图中的 13 行代码,以及17行的变量未进行初始化都没法经过编译。学习

        

 

  ②修饰引用spa

   某个被final修饰的引用一旦被初始化指向一个对象后,就不能够将它改成指向另外一个对象,须要注意的是该对象自己所属的类行为是不会受到限制的。线程

 例以下面的list,初始化后若是想要修改其引用则没法经过编译,可是strList对象对于的List类的行为是不会受到改变的,如add方法

 


                                             

    

 

这里只是限制了引用不可变,还有更狠的操做,JDK 9 以后出现的List.of 方法建立的“不可变list”连list里面的“内容”都不能变了。

下面是JDK 9 中的“新番“:

           

 

                                             

    

 

 

 

 

List.of 方法建立的list是“不可变”的,若是试图去修改不可变list中的内容则会抛出异常;

另外你们实际开发中常见的问题,匿名内部类访问外部类中的局部变量时,为何要将该变量声明为final类型的?(JDK8 以后不须要手动添加final关键字了)

缘由:匿名内部类对象的生命周期比外部类中的局部变量长;

  局部变量的生命周期:当有方法调用并使用到该变量时,变量入栈,方法执行结束后,出栈,变量就销亡了;

  对象的生命周期:当没有引用指向这个对象,GC会在某个时候将其回收,也就是销毁了。

问题:成员方法执行完了,局部变量销毁了,可是对象还仍然存活(没有被GC),这时候对象要去引用该局部变量就引用不到了。

解决方法:java中的内部类访问外部变量时,必须将该变量声明为final,而且inner class会copy一份该变量,而不是直接去使用该局部变量,这样就能够防止数据不一  致的问题了。

java的改进:JDK8 后,若是有内部类访问局部变量,java会自动将该变量修饰成final类型的,因此咱们不须要再去手动添加该关键字。

 

     2.2 修饰方法

 表示该方法不能够被重写(override);比较简单就不展开了。

 

     2.3 修饰类;

 表示该类不能够被继承扩展;

 

这些相信你们都已经掌握了,最关键的是final关键字修饰的字段在内存方面有什么影响?

 

  3.final的内存语义

Oracle官方对于final的说明: https://docs.oracle.com/javase/specs/jls/se7/html/jls-17.html#jls-17.5   

注:还能够去看看《Java并发编程的艺术》 P55 ;对这个官方文档进行了很好的说明及补充;

总结以下:

Java内存模型规定了

第一条,对于final变量的初始化重排序规则:   final 关键字修饰的变量初始化的代码 不能重排序到构造函数结束以后;           

第二条,对于final变量的读取重排序规则:       初次读对象引用与初次读该对象包含的final 域,JMM禁止处理器重排序这两个操做。而这两个操做间存在依赖关系,通常编译器遵照间接依赖关系,不会对其进行重排序。大多数处理器也会遵照间接依赖原则,不会对其重排序。(少数傻吊会对其重排序。。。后面会讲到)

 

首先第一条是什么意思呢?来看看官方的例子

场景: 这个例子中定义了一个final 变量 x 和一个普通变量 y ;在构造函数中赋值。  此时有写和读两个线程开始分别调用writer() 和reader()方法;

最后的结果你猜猜有多少种可能呢?

状况 结果
正常状况 i = 3;  y =4
非正常状况 i = 3; y = 0

为何会出现这种非正常状况呢?

由于我前面说到的是final关键字修饰的变量才能确保不会被重排序到构造函数以后。 普通变量就没这待遇了。

因此通过编译器和处理器重排序后的代码的非正常状况就是这样的:

                                                        

写线程 读线程
1. 构造函数开始执行;  
2. 构造函数中给 final 变量赋值为3;  
3. 构造函数执行结束;  
4.将构造对象的引用赋值给引用变量f  
  1.读取初始化完成的对象
  2.读取该对象中的普通变量 y (有问题)
5.给普通变量y 赋值 为4  

结论: 对于空白final 变量在构造函数中的初始化 代码 不能够重排序到 构造函数以后,必须在构造函数里面完成初始化,  普通变量在不改变单线程运行结果的状况下的初始化能够重排序到构造函数以后。

 

第二条啥意思呢?上面讲到有少数“傻吊”处理器会对 读对象和 读对象中的变量操做进行重排序。

场景:

有一个写线程和一个读线程;

读线程的操做:

正常读取 重排序后的读取
1.读取对象obj 1. 读取obj中的普通变量(问题)
2.读取obj中的普通变量 2.读取对象obj
3.读取obj中的final变量 3.读取对象obj中的final变量
   

重排序后的读取问题在于  读取普通变量 时该普通域还未被初始化,因此读取到的数据时不对的,可是JMM对于final变量读取限制了必须先要读取包含它的对象,而后再去读取该final变量;

 总结: 其实一个小小的final关键字包含的内容是很是多的,这背后为了数据一致性考量的大佬们,在编译器和处理器层面制定了各类规则,因此咱们才能用的方便,喜欢刨根问底的朋友能够参考下面的文档,最后呢,但愿你们多多交流哈,若是有什么问题请帮忙指出!多谢!

 

1. https://docs.oracle.com/javase/specs/jls/se7/html/jls-17.html#jls-17.4       (oracle官方对于final内存语义的说明) 

2. 《并发编程的艺术》  P55

3. Java编程思想,fianl关键字

4.https://en.wikipedia.org/wiki/Final_(Java)#Final_and_inner_classes

 

相关文章
相关标签/搜索