文章来源:《Java并发编程的艺术》

final 域的重排序规则

对于 final 域,编译器和处理器要遵守两个重排序规则。

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

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

下面通过一些实例性的代码来分别说明这两个规则。

public class FinalExample {

int i; // 普通变量

final int j; // final 变量

static FinalExample obj;

public FinalExample() { // 构造函数

i = 1;// 写普通域

j = 2;// 写 final 域

}

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

obj = new FinalExample();

}

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

FinalExample object = obj; // 读对象引用

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

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

}

}

写 final 域的重排序规则

写 final 域的重排序规则禁止把 final 域的写重排序到构造函数之外。这个规则的实现包含下面两个方面:

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

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

现在让我们分析 writer() 方法。writer() 方法只包含一行代码:finalExample = new FinalExample( )。这行代码包含两个步骤,如下:

1)构造一个 FinalExample 类型的对象;

2)把这个对象的引用赋值给引用变量 obj。

假设线程 B 读对象引用与读对象的成员域之间没有重排序(马上会说明为什么需要这个假设),下图是一种可能的执行时序。

在上图中,写普通域的操作被编译器重排序到了构造函数之外,读线程 B 错误地读取了普通变量 i 初始化之前的值。而写 final 域的操作,被写 final 域的重排序规则“限定”在了构造函数之内,读线程 B 正确地读取了 final 变量初始化之后的值。

写 final 域的重排序规则可以确保:在对象引用为任意线程可见之前,对象的 final 域已经被正确初始化过了,而普通域不具有这个保障。以上图为例,在读线程 B “看到” 对象引用 obj 时,很可能 obj 对象还没有构造完成(对普通域 i 的写操作被重排序到构造函数外,此时初始值 1 还没有写入普通域 i)。

读 final 域的重排序规则

读 final 域的重排序规则是,在一个线程中,初次读对象引用与初次读该对象包含的 final 域,JMM 禁止处理器重排序这两个操作(注意,这个规则仅仅针对处理器)。编译器会在读 final 域操作的前面插入一个 LoadLoad 屏障。

初次读对象引用与初次读该对象包含的 final 域,这两个操作之间存在间接依赖关系。由于编译器遵守间接依赖关系,因此编译器不会重排序这两个操作。大多数处理器也会遵守间接依赖,也不会重排序这两个操作。但是有少数处理器允许对存在间接依赖关系的操作做重排序(比如 alpha 处理器),这个规则就是专门用来针对这种处理器的。

read() 方法包含 3 个操作。

初次读引用变量 obj。

初次读引用变量 obj 指向对象的普通域 j。

初次读引用变量 obj 指向对象的 final 域 i。

现在假设写线程 A 没有发生任何重排序,同时程序在不遵守间接依赖的处理器上执行,下图所示是一种可能的执行时序。

在上图中,读对象的普通域的操作被处理器重排序到读对象引用之前。读普通域时,该域还没有被写线程 A 写入,,这是一个错误的读取操作。而读 final 域的重排序规则会把读对象 final 域的操作“限定”在读对象引用之后,此时该 final 域已经被 A 线程初始化过了,这是一个正确的读取操作。

读 final 域的重排序规则可以确保:在读一个对象的 final 域之前,一定会先读包含这个 final 域的对象的引用。在这个实例程序中,如果该引用不为 null,那么该引用对象的 final 域一定已经被 A 线程初始化过了。

final 域为引用类型

上面我们看到的 final 域是基础数据类型,如果 final 域是引用数据类型,将会有什么效果?请看下面示例代码。

public class FinalRefrenceExample {

final int[] intArray; // final 是引用类型

static FinalReferencExample obj;

public FinalReferenceExample() { // 构造函数

intArray = new int[1]; // 1

intArray[0] = 1; // 2

}

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

obj = new FinalReferenceExample(); // 3

}

public static void writerTwo() { // 写线程B执行

obj.intArray[0] = 2; // 4

}

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

if (obj != null) { // 5

int temp1 = obj.intArray[0]; // 6

}

}

}

本例 final 域为一个引用类型,它引用一个 int 型的数组对象。对于引用类型,写 final 域的重排序规则对编译器和处理器增加了如下约束:在构造函数内对一个 final 引用的对象的成员域的写入,与随后在构造函数外把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。

对上面的示例程序,假设首先线程 A 执行 writerOne() 方法,执行完后线程 B 执行 writerTwo() 方法,执行完后线程 C 执行 reader() 方法。下图是一种可能的线程执行时序。

在上图中,1是对 final 域的写入,2 是对这个 final 域引用的对象的成员域的写入,3是把被构造的对象的引用赋值给某个引用变量。这里除了前面提到的 1 不能和 3 重排序外,2 和 3 也不能重排序。

JMM 可以确保读线程 C 至少能看到写线程 A 在构造函数中对 final 引用对象的成员域的写入。即 C 至少能看到数组下标 0 的值为 1。而写线程 B 对数组元素的写入,读线程 C 可能看得到,也可能看不到。JMM 不保证线程 B 的写入对读线程 C 可见,因为写线程 B 和读线程 C 之间存在数据竞争,此时的执行结果不可预知。

如果想要确保读线程 C 看到写线程 B 对数组元素的写入,写线程 B 和读线程 C 之间需要使用同步原语(lock 或 volatile)来确保内存可见性。

为什么 final 引用不能从构造函数中逸出

前面我们提到过,写 final 域的重排序规则可以确保:在引用变量为任意线程可见之前,该引用变量指向的对象的 final 域已经在构造函数中被正确初始化过了。其实,要得到这个效果,还需要一个保证:在构造函数内部,不能让这个被构造函数的引用为其他线程所见,也就是对象引用不能在构造函数中“逸出”。为了说明这个问题,让我们来看下面的示例代码:

public class FinalReferenceEscapeExample {

final int i;

static FinalReferenceEscapeExample obj;

public FinalReferenceEscapeExample() {

i = 1;// 1 写 final 域

obj = this;// 2 this 引用在此“逸出”

}

public static void writer() {

new FinalReferenceEscapeExample();

}

public static void reader() {

if (obj != null) { // 3

int temp = obj.i; // 4

}

}

}

假设一个线程 A 执行 writer() 方法,另一个线程 B 执行 reader() 方法。这里的操作 2 使得对象还未完成构造前就为线程 B 可见。即使这里的操作 2 是构造函数的最后一步,且在程序中操作 2 排在操作 1 后面,执行 read() 方法的线程仍然可能无法看到 final 域被初始化后的值,因为这里的操作 1 和 操作 2 之间可能被重排序。实际的执行时序可能如下图所示:

在上图中:在构造函数返回前,被构造对象的引用不能为其他线程所见,因为此时的 final 域可能还没有被初始化。在构造函数返回后,任意线程都将保证能看到 final 域正确初始化之后的值。

JSR-133 为什么要增强 final 语义

在旧的 Java 内存模型中,一个最严重的缺陷就是线程可能看到 final 域的值会改变。比如,一个线程当前看到一个整型 final 域的值为 0(还未初始化之前的默认值),过一段时间之后这个线程再去读这个 final 域的值时,却发现值变为 1(被某个线程初始化之后的值)。最常见的例子就是在旧的 Java 内存模型中,String 的值可能会发生改变。

为了修补这个漏洞,JSR-133 专家组增强了 final 的语义。通过为 final 域增加写和读重排序规则,可以为 Java 程序员提供初始化安全保证:只要对象是正确构造的(被构造对象的引用在构造函数中没有“逸出”),那么不需要使用同步(指 lock 和 volatile 的使用)就可以保证任意线程都能看到这个 final 域在构造函数中被初始化后的值。

java final域_【Java】final 域的内存语义相关推荐

  1. java取负数_[Java] 告别“CV 工程师”码出高效!(基础篇)

    作为一名资深的 CV 工程师,某天,当我再一次日常看见满屏的报错信息与键盘上已经磨的泛白的 Ctrl.C.V 这三个按键时,我顿悟了. 百度谷歌复制粘贴虽然很香,但是总是依靠前人种树,终会有一天失去乘 ...

  2. java jpa性能_[Java Performance] 数据库性能最佳实践 - JPA和读写优化

    数据库性能最佳实践 当应用须要连接数据库时.那么应用的性能就可能收到数据库性能的影响. 比方当数据库的I/O能力存在限制,或者因缺失了索引而导致运行的SQL语句须要对整张表进行遍历.对于这些问题.只相 ...

  3. java控制语句练习题_[Java初探实例篇02]__流程控制语句知识相关的实例练习

    本例就流程控制语句的应用方面,通过三个练习题来深入学习和巩固下学习的流程控制语句方面的知识,设计到,if条件判断语句,switch多分支语句,for循环语句及其嵌套多层使用,while循环语句. 练习 ...

  4. java 文本压缩_[Java基础]Java使用GZIP进行文本压缩

    import java.io.IOException; import java.util.zip.GZIPOutputStream; import org.apache.commons.io.outp ...

  5. java private 对象_[Java笔记]类的所有构造器都是private权限,就一定没有办法实例化它的对象了么?...

    笔者以前学过C++语言.众所周知,C++也是一门面向对象程序设计语言.还记得当时在大学的时候,老师讲过这样的话:类的构造函数不应该设置成private权限,这样的话还怎么去实例化类的对象?当时也信以为 ...

  6. java resources 目录_[Java] 在 jar 文件中读取 resources 目录下的文件

    注意两点: 1. 将资源目录添加到 build path,确保该目录下的文件被拷贝到 jar 文件中. 2. jar 内部的东西,可以当作 stream 来读取,但不应该当作 file 来读取. 例子 ...

  7. java斗地主发牌_[Java源码]扑克牌——斗地主发牌实现

    --------------------------------------- --------------------------------------- ----------一个扑克牌核心和简单 ...

  8. java小朋友猜拳_[Java教程]Java猜拳小游戏(剪刀、石头、布)

    [Java教程]Java猜拳小游戏(剪刀.石头.布) 0 2015-09-29 08:00:04 import java.util.Random;import java.util.Scanner;pu ...

  9. java星空屏幕_[Java教程]窗口设置_星空网

    窗口设置 2016-04-13 0 /** * 这个是GUI的事例程序: * */ package w160412.wang.main;import java.awt.Color; import ja ...

  10. java中并行_[JAVA] 12. Java中的并行Concurrency

    启用线程 public static void show() { System.out.println(Thread.currentThread().getName()); for (var i : ...

最新文章

  1. 启明云端分享| ESP32-S3支持自定义离线语音,可支持 200 条本地命令语句,无需外加 DSP 芯片
  2. 让Android Studio支持系统签名(证书)
  3. BugkuCTF-WEB题网站被黑
  4. C++中的深拷贝和浅拷贝(详解)
  5. Web前端开发——BAT面试题汇总及答案01
  6. SurfaceView实例
  7. 共享一个从字符串转 Lambda 表达式的类(2)
  8. 电影图标:杀死比尔(Kil Bill)
  9. (转)webstorm快捷键
  10. 快鲸六大私域运营服务,赋能企业业绩长效增长
  11. 从自走棋代码分析游戏机制--棋池、回蓝、目标判断、掉落概率与新英雄
  12. Springboot物理地址映射和Nginx静态资源代理实现前端上传并访问服务器图片
  13. CentOS 8 更新/etc/yum.repos.d
  14. 获得淘系商品详情展示
  15. 一篇文章带你认识数学建模中的方程与方程组
  16. 大数据开发常用的编程语言有哪些
  17. 「分辨率比拼」还不够,4D成像雷达进入“软”竞争时代
  18. 算法设计 (分治法应用实验报告)基于分治法的合并排序、快速排序、最近对问题
  19. bind()函数介绍
  20. GUC-6 Callable 接口

热门文章

  1. 设置占用GPU的比例
  2. [Common 17-39] ‘connect_bd_intf_net‘ failed due to earlier errors. 的解决办法
  3. matlab简单分析频域滤波和时域滤波
  4. markdown与latex:书写单边大括号左边或右边即在没有括号的一端加点
  5. ubuntu 开启ssh
  6. Linux下如何查看定位当前正在运行的Nginx的配置文件
  7. c++ -- 重载、重写(覆盖)和隐藏的区别
  8. C++ 中的深入浅拷贝和深拷贝
  9. TeeChart的坐标轴
  10. [家里蹲大学数学杂志]第187期实数集到非负实数集的双射有无穷多个间断点