本文最后更新于:2020年9月15日 晚上

Java并发系列(十二)-final域的内存语义

本篇主要包含以下内容:

  • 1、final域的重排序规则
  • 2、写final域的重排序规则
  • 3、读final域的重排序规则
  • 4、final域为引用类型
  • 5、final引用不能从构造函数中“溢出”
  • 6、final引用的原则
  • 7、JSR-133为什么要增加final的语义

1、final域的重排序规则

1.发生在构造函数中写final域。

    在一个构造函数内对一个final域的写入,
    与随后把这个【被构造对象】(类对象)的引用复制给一个引用变量,
    这两个操作不能发生重排序。

2.读final域。

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

2、写final域的重排序规则

禁止final域的写重排序到构造函数之外。

2.1、final域写的重排序规则

写final域的重排序规则是禁止把final域的写重排序到构造函数之外。

2.2、final域写的重排序规则实现

同时禁止编译器和处理器将final域的写重排序到构造函数之外。

1.JMM禁止编译器把final域的写重排序到构造函数之外。
2.编译器会在final域的写之后,return之前插入storestore屏障。
这个屏障会禁止处理器把final域的写重排序到构造函数之外。

2.3、写final域重排序规则的作用

确保对象引用在被任何线程可见时,
对象的final域已经被正确初始化过了,而普通域不具备这个保障。

3、读final域的重排序规则

final域的对象引用->final域

3.1、final域读的重排序规则

在一个线程中,初次读一个对象引用与初次读该对象包含的final域,
这两个操作的重排序是JMM禁止处理器这样做的。

3.2、final域读的重排序规则实现

在final域读操作的前面加上LoadLoad屏障。

3.3、读final域重排序规则的作用

在读一个对象的final域之前,会先读包含这个final域的对象的引用。

4、final域为引用类型

4.1、final域写的重排序规则

final引用的对象的成员域写入->该引用对象赋值给另一个引用变量

在构造函数对一个final引用对象的成员域(数组就是数组元素)写入,
与随后在构造函数外把这个被构造对象的引用赋值给一个引用变量,
这个操作之间不能发送重排序。

4.2、解决数据竞争内存可见性问题

通过在线程之间使用同步原语(lock或volatile)

5、final引用不能从构造函数中“溢出”

目的:可以进一步确保写final域的重排序规则。

不能让final引用从构造函数中“溢出“指的是:

在构造函数内部,不能让这个被构造对象的引用为其他线程所见。

6、final引用的原则

在构造函数返回前,被构造对象的引用不能为其他线程所见,因为此时的final域可能还没有被初始化。

在构造函数返回后,任意线程将保证能看到final域,正确初始化之后的值。

7、JSR-133为什么要增加final的语义

原因:

    在旧的Java内存模型中,一个最严重的缺陷就是线程可能看到final的值会改变。
    (简单的说就是不同时刻看到的值是不一样的)

解决:

    通过增强final域的写和读的重排序规则,来增强final的语义。

达到目的:

    只要对象是正确构造的,那么不需要使用同步,
    就能保证任意线程能看到这个final域,
    在构造函数中被初始化之后的值。

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!博客中转载文章会注明出处,若有版权问题,请及时与我联系!谢谢!

Java并发系列(十三)-happens-before 上一篇
Java并发系列(十一)-锁的内存语义 下一篇