写屏障是什么_面试官为什么问内存模型总离不开final关键字,该如何应对?

3b9e247736d02e0a1d5ed96e14329617.png

Java 语言的每个关键字都设计的很巧妙,金雕玉琢,只有深度钻研其中,才知其中懊悔,本文带领大家一起深入理解 Java 内存模型之 final。

加我微信好友的不要着急,手机没电了,等我借个充电器之后,再一一通过!

与前面介绍的锁和 volatile 相比较,对 final 域的读和写更像是普通的变量访问。对于 final 域,编译器和处理器要遵守两个重排序规则:

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

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

下面,我们通过一些示例性的代码来分别说明这两个规则:

public class FinalExample {int i;                            // 普通变量final int j;                      //final 变量static FinalExample obj;public void 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 域}}

这里假设一个线程 A 执行 writer () 方法,随后另一个线程 B 执行 reader () 方法。下面我们通过这两个线程的交互来说明这两个规则。

# 写 final 域的重排序规则

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

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

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

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

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

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

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

ed2a6b056c7d0ddb13adb0d23ec38174.png

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

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

# 读 final 域的重排序规则

读 final 域的重排序规则如下:

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

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

reader() 方法包含三个操作:

  1. 初次读引用变量 obj;

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

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

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

2c9e0a08a2d48eb69bd3621a8f8b508c.png

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

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

# 如果 final 域是引用类型

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

请看下列示例代码:

public class FinalReferenceExample {final int[] intArray;                     //final 是引用类型static FinalReferenceExample obj;public FinalReferenceExample () {        // 构造函数        intArray = new int[1];              //1        intArray[0] = 1;                   //2}public static void writerOne () {          // 写线程 A 执行        obj = new FinalReferenceExample ();  //3}public static void writerTwo () {          // 写线程 B 执行        obj.intArray[0] = 2;                 //4}public static void reader () {              // 读线程 C 执行if (obj != null) {                    //5int temp1 = obj.intArray[0];       //6}}}

这里 final 域为一个引用类型,它引用一个 int 型的数组对象。对于引用类型,写 final 域的重排序规则对编译器和处理器增加了如下约束:

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

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

423b3d4154280afd5c2b770c4697f777.png

在上图中,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) {                     //3int temp = obj.i;                 //4}}}

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

6de4d47ed9afb4c6bd5c07b2dd4e9b91.png

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

# final 语义在处理器中的实现

现在我们以 x86 处理器为例,说明 final 语义在处理器中的具体实现。

上面我们提到,写 final 域的重排序规则会要求译编器在 final 域的写之后,构造函数 return 之前,插入一个 StoreStore 障屏。读 final 域的重排序规则要求编译器在读 final 域的操作前面插入一个 LoadLoad 屏障。

由于 x86 处理器不会对写 - 写操作做重排序,所以在 x86 处理器中,写 final 域需要的 StoreStore 障屏会被省略掉。同样,由于 x86 处理器不会对存在间接依赖关系的操作做重排序,所以在 x86 处理器中,读 final 域需要的 LoadLoad 屏障也会被省略掉。也就是说在 x86 处理器中,final 域的读 / 写不会插入任何内存屏障!

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

在旧的 Java 内存模型中 ,最严重的一个缺陷就是线程可能看到 final 域的值会改变。比如,一个线程当前看到一个整形 final 域的值为 0(还未初始化之前的默认值),过一段时间之后这个线程再去读这个 final 域的值时,却发现值变为了 1(被某个线程初始化之后的值)。最常见的例子就是在旧的 Java 内存模型中,String 的值可能会改变(参考文献 2 中有一个具体的例子,感兴趣的读者可以自行参考,这里就不赘述了)。

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

编辑:业余草
来源:https://www.xttblog.com/?p=4957

 往期推荐 

?

  • 阿里面试官:数据库连接池有必要吗?你对它的底层实现了解过没?
  • 代码,到底该如何分层,才能给人赏心悦目的感觉?
  • 你这代码写得真丑,满屏的try-catch,全局异常处理不会吗?

d354934384c0da022ea406702d86df8c.png

f4a324761a7184e5fd605118a4f8ca45.gif 

点击

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/349241.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

非静态方法可以访问Java中的静态变量/方法吗?

“非静态方法可以访问静态变量或调用静态方法”是Java中有关静态修饰符的常见问题之一,答案是, 是的 ,非静态方法可以访问静态变量或调用静态方法。 Java中的方法。 这没有问题,因为有静态成员,即静态变量和静态方法都…

which oracle linux,(总结)Linux下Oracle11gR2的ORA-00845错误解决方法

PS:前些时间一台演示环境的Oracle 11g for Linux不知什么原因,启动不起来,报错ORA-00845。搜索了下,这个问题是由于设置SGA的大小超过了操作系统/dev/shm的大小。当时解决了没空写总结,今天有点空,总结分享…

oracle存储过程深入,深入了解oracle存储过程的优缺点

定义:存储过程(Stored Procedure )是一组为了完成特定功能的SQL 语句集,经编译后存储在数据库中。用户通过指定存储过程的名字并给出参数(如果该存储过程带有参数)来执行它。存储过程是数据库中的一个重要对象,任何一个设计良好的数据库应用程…

如何在Java 8中使用LocalDateTime格式化/解析日期-示例教程

Java项目中的常见任务之一是将日期格式化或解析为String,反之亦然。 解析日期表示您有一个表示日期的字符串,例如“ 2017-08-3”,并且要将其转换为表示Java中日期的对象,例如Java 8之前版本中的java.util.Date以及LocalDate或Loca…

如何获取当前刀具号_数控刀具的选用原则,如何使用数控刀具?一文全面介绍数控刀具...

数控刀具选用概述学习数控相关知识,最基础的是认识和了解刀具的材料以及选用原则,我们应当了解数控刀具的种类及特点、如何正确选择和使用数控加工刀具;学会根据被加工材料来合理选择数控刀具的材料和切削参数。选用原则:数控车床…

Java命令行界面(第27部分):cli-parser

CLI Parser最初托管在Google Code上,现在已存档在Google Code上 ,现在可以在GitHub上使用 。 归档的Google Code项目页面将CLI解析器描述为“使用非常简单,非常小的依赖项”,它使用注释“使非常简洁的主要方法不需要知道如何解析带…

使用2.26内核的linux,介绍linux 2.6.9-42内核升级到linux 2.6.26-42的方法

介绍linux 2.6.9-42内核升级到linux 2.6.26-42的方法来源:互联网作者:佚名时间:2013-04-10 13:32这篇升级Linux内容的文章,是基于Red Hat的Linux版本,从linux 2.6.9-42内核升级到linux 2.6.26-42的方法,对于…

Java命令行界面(第1部分):Apache Commons CLI

尽管我通常使用Groovy编写要从命令行运行的JVM托管脚本,但是有时候我需要解析Java应用程序中的命令行参数,并且有很多库可供Java开发人员用来解析命令行参数。 在本文中,我将介绍这些Java命令行解析库中最著名的一种: Apache Comm…

丙烯怎么做成流体丙烯_韧性好强度高的聚丙烯复合材料怎么做?让人工智能来帮忙...

01背景介绍聚丙烯(PP)是一种应用广泛的通用塑料,价格便宜、力学性能好、热稳定性高,在机械、汽车、电子电器、建筑、纺织、包装和食品工业等领域应用广泛。聚丙烯韧性和冲击强度不高,限制了它的应用。加入热塑性弹性体(TPE),如苯乙…

vivado安装_Vivado下载与安装指南

Vivado下载与安装指南目前,vivado已推出2019.1版本,实验室所安装的为2018.3版本,由于软件向下兼容的特性,建议安装2018版本,若安装2019版本,请自带笔记本,安装过程与之前没有差别,这…

嵌入式基于linux电机控制器,基于嵌入式Linux的移动机器人控制系统

使用select机制监控是否语音识别结果,在超出等待时间后,会退出等待并重新初始化语音模块LD3320,释放公共资源,这样也使得系统能够及时响应LD3320的MP3播放功能,避免了在长时间没有语音识别结果时,系统进入卡…

windows server 驱动精灵_还在用Windows文件共享?我来教你一键摆脱Windows海量小文件使用和备份的噩梦...

每当我问到客户,“你用什么存储产品作为文件共享?”经常听到的一个答案(自豪滴)是,“文件共享需要存储么?我们用Windows就可以做到。”Windows就是个百宝箱,什么都能往里装,就像你家冰箱一样。众所周知&…

pb 应用 迁移 linux_功能化生物炭应用研究取得系列进展

土壤营养元素流失、重金属污染是当前全球面临的突出环境问题。生物炭因其具有比表面积较大、吸附性能高和成本低等优点而在环境修复领域日益受到广泛关注,被作为水处理吸附剂、土壤修复改良剂广泛应用于农业土壤改良和环境中重金属的修复和钝化。但通常情况下&#…

Java命令行界面(第26部分):CmdOption

由于Tweet,我了解了本系列中第26个基于Java的功能强大的库,该库用于解析命令行参数 。 CmdOption在其GitHub主页上被描述为“一个通过注释配置的,用于Java 5应用程序的简单注释驱动的命令行解析器工具包。” 该项目的副标题是“命令行解析从未…

vector c++ 赋值_面对拷贝赋值时发生的自我赋值的正确态度时接受而不是防止

C.62: Make copy assignment safe for self-assignmentC.62:保证拷贝赋值对自我赋值安全Reason(原因)If x x changes the value of x, people will be surprised and bad errors will occur (often including leaks).如果xx改变了x的值,人们会觉得很奇怪&#xff0…

华为编程规范_华为 Java 编程规范出炉,究竟和官方文档有何不同?

来源:blog.csdn.net/chenleixing/article/details/441739851、引言这个标准是衡量代码本身的缺陷,也是衡量一个研发人员本身的价值。华为作为一家全球化的 IT 公司,十几万员工,无论是人事管理,还是代码管理&#xff0c…

Java命令行界面(第19部分):jClap

本系列中第19篇文章的重点是从Java代码解析命令行参数是jClap ( Java命令行参数解析器 ),不应将它与称为JCLAP的库相混淆,而JCLAP库是我本系列先前文章的重点。 在以前的帖子覆盖JCLAP 1.4加尔斯吉尔温斯坦利( snaq.ne…

Java命令行界面(第22部分):argparser

John Lloyd的argparser是本系列的第二十二篇有关基于Java的命令行参数解析的文章中介绍的库。 该库的主页除了提供单个源代码示例外,还提供了指向基于Javadoc的API文档 ,JAR文件,ZIP文件和TAR文件的链接。 本帖子中使用的示例与本系列的前二十…

如何粗暴地下载huggingface_hub指定数据文件

参考这里: https://huggingface.co/docs/huggingface_hub/guides/download 可见下载单个文件,下载整个仓库文件都是可行的。 这是使用snapshot_download下载的一个例子: https://qq742971636.blog.csdn.net/article/details/135150482 sn…

顺序表输入栈元素c语言,C语言数据结构之栈简单操作

C语言数据结构之栈简单操作实验:编写一个程序实现顺序栈的各种基本运算,并在此基础上设计一个主程序,完成如下功能:(1)初始化顺序栈(2)插入元素(3)删除栈顶元素(4)取栈顶元素(5)遍历顺序栈(6)置空顺序栈分析:栈的顺序存储结构简称…