写点什么

final 的两个重排序规则

  • 2022 年 5 月 07 日
  • 本文字数:1910 字

    阅读完需:约 6 分钟

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


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


先看一段示例代码:


package com.test;


/**


  • final 域为基本类型

  • @author riemann

  • @date 2019/07/17 23:24


*/


public class FinalDemo {


private int a; //普通变量


private final int b; //final 变量


private static FinalDemo finalDemo;


public FinalDemo() { //构造函数


a = 1; //写普通域


b = 2; //写 final 域


}


public static void writer() { //写线程 A 执行


finalDemo = new FinalDemo();


}


public static void reader() { //读线程 B 执行


FinalDemo object = finalDemo; //读对象引用


int a = object.a; //读普通域


int b = object.b; //读 final 域


}


}


假设线程 A 在执行 writer()方法,线程 B 执行 reader()方法。


[](()一、final 域为基本类型




(一)、写 final 域重排序规则


写 final 《一线大厂 Java 面试题解析+后端开发学习笔记+最新架构讲解视频+实战项目源码讲义》无偿开源 威信搜索公众号【编程进阶路】 域的重排序规则禁止对 final 域的写重排序到构造函数之外,这个规则的实现主要包含了两个方面:


1、JMM 禁止编译器把 final 域的写重排序到构造函数之外;


2、编译器会在 final 域写之后,构造函数 return 之前,插入一个 storestore 屏障(关于内存屏障可以看[这篇文章](())。这个屏障可以禁止处理器把 final 域的写重排序到构造函数之外。


我们再来分析 writer 方法,虽然只有一行代码,但实际上做了两件事情:


1、构造了一个 FinalDemo 对象;


2、把这个对象赋值给成员变量 finalDemo。


我们来画下存在的一种可能执行时序图,如下:



由于 a,b 之间没有数据依赖性,普通域(普通变量)a 可能会被重排序到构造函数之外,线程 B 就有可能读到的是普通变量 a 初始化之前的值(零值),这样就可能出现错误。而 final 域变量 b,根据重排序规则,会禁止 final 修饰的变量 b 重排序到构造函数之外,从而 b 能够正确赋值,线程 B 就能够读到 final 变量初始化后的值。


因此,写 final 域的重排序规则可以确保:在对象引用为任意线程可见之前,对象的 final 域已经被正确初始化过了,而普通域就不具有这个保障。比如在上例,线程 B 有可能就是一个未正确初始化的对象 finalDemo。


( 二)、读 final 域重排序规则


读 final 域重排序规则为:在一个线程中,初次读对象引用和初次读该对象包含的 final 域,JMM 会禁止这两个操作的重排序。(注意,这个规则仅仅是针对处理器),处理器会在读 final 域操作的前面插入一个 LoadLoad 屏障。实际上,读对象的引用和读该对象的 final 域存在间接依赖性,一般处理器不会重排序这两个操作。但是有一些处理器会重排序,因此,这条禁止重排序规则就是针对这些处理器而设定的。


read()方法主要包含了三个操作:


1、初次读引用变量 finalDemo;


2、初次读引用变量 finalDemo 的普通域 a;


3、初次读引用变量 finalDemo 的 final 与 b;


假设线程 A 写过程没有重排序,那么线程 A 和线程 B 有一种的可能执行时序为下图:



读对象的普通域被重排序到了读对象引用的前面就会出现线程 B 还未读到对象引用就在读取该对象的普通域变量,这显然是错误的操作。而 final 域的读操作就“限定”了在读 final 域变量前已经读到了该对象的引用,从而就可以避免这种情况。


读 final 域的重排序规则可以确保:在读一个对象的 final 域之前,一定会先读这个包含这个 final 域的对象的引用。


[](()二、final 域为引用类型




我们已经知道了 final 域是基本数据类型的时候重排序规则是怎么的了?如果是引用数据类型了?我们接着继续来探讨。


(一)、对 final 修饰的对象的成员域写操作


针对引用数据类型,final 域写针对编译器和处理器重排序增加了这样的约束:在构造函数内对一个 final 修饰的对象的成员域的写入,与随后在构造函数之外把这个被构造的对象的引用赋给一个引用变量,这两个操作是不能被重排序的。注意这里的是“增加”也就说前面对 final 基本数据类型的重排序规则在这里还是使用。这句话是比较拗口的,下面结合实例来看。


package com.test;


/**


  • final 域为引用类型

  • @author riemann

  • @date 2019/07/18 0:33


*/


public class FinalReferenceDemo {


final int[] arrays;


private FinalReferenceDemo finalReferenceDemo;


public FinalReferenceDemo() {


arrays = new int[1]; //1


arrays[0] = 1; //2


}


public void writerOne() {


finalReferenceDemo = new FinalReferenceDemo(); //3


}


public void writerTwo() {


arrays[0] = 2; //4


}


public void reader() {


if (finalReferenceDemo != null) { //5

用户头像

还未添加个人签名 2022.04.13 加入

还未添加个人简介

评论

发布
暂无评论
final的两个重排序规则_程序员_爱好编程进阶_InfoQ写作社区