聊一聊final关键字

2018-08-12  本文已影响0人  Java技术范

01.相关概念

    final关键字主要用来修饰类、方法和字段;当修饰类的时候,表示该类是不可继承的;当修饰方法的时候,表示该方法不可重写;当修饰字段的时候,表示该字段内容不可更改。

    相信对于Java基础比较好的以上的几点,相信大家都很熟悉了;但是在JMM中,final修饰的字段是禁止了一些重排序的

02.重排序规则

    所谓重排序,在JMM中,有3中重排序:编译级重排序规则、指令级重排序规则和内存重排序规则。JMM为了提高Java程序性能,允许一些重排序规则,但是一些重排序规则会改变程序的结果。这个时候,我们使用一些工具来禁止JMM的重排序规则,JMM将这些工具称之为内存屏障。

    内存屏障:简单的理解,就是一组CPU指令。那么JMM定义了哪些内存屏障呢?

    在上述内存屏障中,StoreLoad 屏障是一个万能屏障,几乎拥有前3种屏障的所有功能。下面我们将会分析一下final关键字的内存语义和禁止重排序规则。

03.final域内存语义

    我们先来说明针对于final关键字的两条重排序规则:

    1.在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。


    2.初次读一个包含final域的对象引用,与随后初次读这个final域,这两个操作之间不能重排序。

    我们来看一下下面一段代码

04.final域写内存语义

    

针对于上述代码,这里的执行过程可以是这样的

对于线程A执行的方法,主要分为两步:

  1. 就是在堆内存区域开辟空间,进行构造函数对对象初始化、   

  2. 将引用赋值给obj变量

针对于上述final域,final域的写,我们可以看到在构造函数结束之前插入了StoreStore内存屏障,JMM无法将final域的写重排序到构造函数之外。但是对于一般的变量,为了提高Java程序的性能,允许将普通变量的写重排序到构造函数之外。

针对于final域的写,这里有两条规则:

    1.JMM禁止将final域的写入重排序到构造函数之外

    2.编译器将会在final域的写入之后,return语句返回之前插入StoreStore内存屏障StoreStore内存屏障。

    在线程B中,针对于读取对象的实例数据部分,final 域的读取是初始化的值,但是对于一般变量的读取,可能读取的是初始化的旧值。

05.final域的读语义

    在上一节内容中,线程B的执行顺序是一种可能,这里我们将会看到线程B可能另一种执行的顺序,具体如下图所示:

  针对于线程B,程序员认为的操作是这样的:

    1.对去对象引用后赋值给tmp变量

    2.读取普通变量

    3.读取final变量

但是考虑到重排序问题,普通变量的读操作有可能会重排序到方法外面,显然这样的操作是不正确的;但是对于final域的读,一定是在读取final域的实例对象引用后在进行读取的。那么JMM如何实现禁止对于final域读取的重排序呢?   

     编译器会在读final域的前面插入LoadLoad内存屏障。

06.final域是引用类型

    上面分析了final域是基本类型,如果是引用类型呢,将会是什么情况呢?

这里,我们只是做一个总结:

    在构造函数内对一个final引用的对象的成员域的写入,与随后在构造函数外把这个被构造对象的引用赋值给一个引用变量,这两个操作是不能重排序的。

    具体的情况和上述一样,留给读者去思考。

07.为什么要禁止final域的重排序呢?

    禁止final域的重排序是基于这样的考虑:如果一个线程,在读取final域的时候首先读取的final域的值是0,然后再次读取的final域值是1,这样有悖于final修饰的字段是常量的规定,所以在后来的Java规范中,增加了对final域的语义。

上一篇 下一篇

猜你喜欢

热点阅读