写屏障是什么_面试官为什么问内存模型总离不开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中的方法。 这没有问题,因为有静态成员,即静态变量和静态方法都…

php中$_post怎么用,php – 如何在$_POST []中使用变量

我需要遍历一堆动态生成的字段,但这不起作用:$population_density $_POST[$current_location_id];我有一个页面列表,其人口在一页上;我需要这样做,这样你就可以立刻更新它们.所以我使字段名称动态地对应于location_id.提交帖子时,我需要像这样迭代它们,但似乎你不能…

python输入字母终止_将用户输入限制为字母

我是学python的技术作家。我想写一个验证姓名字段输入的程序,作为实践,将用户输入限制为字母。我在这里看到了一个类似的验证数字(年龄)字段的代码,并将其用于字母表,如下所示:import stringimport rer re.compile(r[…

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

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

oracle编程基本语法,oracle编程基础语法

oracle数据开发编程结构: declare[定义变量]begin[逻辑代码]exception[捕获异常]end;实例:declarea number:1;b number:2;c number;beginc:(a*b)/(ab);dbms_output.put_line(c);exceptionwhen zero_divide thendbms_output.put_line(除数不能…

java8optional_关于Java 8的Optional的介绍

java8optional我最近发现了JDK 8中Optional类型的添加。 Optional类型是避免NullPointerException一种方法,因为从方法中获取Optional返回值的API使用者被“强制”执行“在线”检查,以消耗其实际返回值。 更多细节可以在Javadoc中看到。 可以在此博客文章…

python大文件排序_python实现按创建时间对文件排序

测试中,测试log是经常需要保存一段时间以便于后续查询,但是如果一段时间不删除,会导致硬盘空间变小而影响自动化测试,通常空间太小,自动化测试case就不能调用了,或者即使调用,可能会引起新测试的…

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解析器描述为“使用非常简单,非常小的依赖项”,它使用注释“使非常简洁的主要方法不需要知道如何解析带…

linux系统中如何安装qwt,linux下Qt开发环境中qwt库的安装与使用

qwt的安装与使用安装好qt开发环境后,先去下载qwt库源代码。以我下载的qwt-6.0.1.zip为例,解压得到qwt-6.0.1。1.安装qwt-6.0.1执行下面的命令:1 cd qwt-6.0.12 qmake3 make4 make install这样就完成qwt-6.0.1的安装了,安装的路径是…

moxy json介绍_MOXy的对象图和动态JAXB

moxy json介绍JAXB(JSR-222)使您可以轻松地将域类的实例转换为XML。 EclipseLink MOXy实现提供了一个称为Dynamic JAXB的扩展,在其中,您可以使用诸如DynamicEntity之类的映射实例代替真实的类​​。 您可以使用采用属性名称的get和…

linux 查看服务器作业,linux – 如何在服务器负载较低时运行作业?

我有一个运行磁盘快照的命令(在EC2上,冻结XFS磁盘并运行EBS快照命令),该命令设置为作为cron作业定期运行.理想情况下,如果在任务计划运行时磁盘被大量使用,我希望能够将命令延迟一段时间.我担心使用nice / ionice可能没有正确的效果,因为我希望脚本在运行时以高优先级运行(即等…

wordcloud python3.6能用吗_Python3.6环境下安装wordcloud模块遇到的问题及解决办法

“error: Microsoft Visual C 14.0 is required…”问题解决今天在Python 3.6环境中,制作词云,需要安装wordcloud模块,在安装的过程中遇到了“error: Microsoft Visual C 14.0 is required…” 的问题,很是烦恼,最后找…

使用基本身份验证来保护Spring Boot REST API

这是我的Spring Boot Blog帖子系列的第三篇文章。 在第一篇文章中,我谈到了我使用Spring Boot创建RESTFul Services的经验。 然后我将样本扩展到 与Swagger文档集成 。 在这篇文章中,我将在安全方面扩展上述示例。 什么是API安全性 API安全性广泛&…

使用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的方法,对于…

python xlwt xlrd 写入一行_自己总结python用xlrd\xlwt读写excel

1.首先安装xlrd\xlwt模块xlrd模块下载地址:https://pypi.python.org/pypi/xlrdxlwt模块下载地址:https://pypi.python.org/pypi/xlwtlinux 安装命令 python setup.py installwindos 安装命令 setup.py install2.使用方法大全基础方法推荐请看这里:http://blog.csdn.net/mr__fan…

linux时间路径,关于linux中的时间 时区问题

本文部分来源于: http://hi.baidu.com/peruke/blog/item/b8de06ec6a04583b27979132.html系统是fedora:glibc实现了从RTC时间到人可读时间的一个转换,一般系统不适用 环境变量TZ进行时区的设置,因为环境变量存在一些缺陷&#xff…

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

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