final关键字及其内存语义

 

  一、 final  

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

 1.概述

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

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

 

    2.1 修饰变量

    作用:表示该变量不可以被再次赋值修改;

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

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

  Cannot assign a value to final variable 

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

 另外需要注意的是java允许用final修饰某个未进行初始化复制的变量,这叫做“空白final”,但是编译器一定能够确保空白final在使用前被初始化,不然报错给你看   

例如下图中的 13 行代码,以及17行的变量未进行初始化都无法通过编译。

        

 

  ②修饰引用

   某个被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.读取对象obj1. 读取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

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

  

    

 

转载于:https://www.cnblogs.com/michaelwwx/p/10526991.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值