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执行的方法,主要分为两步:
就是在堆内存区域开辟空间,进行构造函数对对象初始化、
将引用赋值给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域的语义。