写点什么

深入理解 Java 内存模型(二)——重排序

  • 2013-01-26
  • 本文字数:2582 字

    阅读完需:约 8 分钟

数据依赖性

如果两个操作访问同一个变量,且这两个操作中有一个为写操作,此时这两个操作之间就存在数据依赖性。数据依赖分下列三种类型:

名称 代码示例 说明 写后读 a = 1;b = a; 写一个变量之后,再读这个位置。 写后写 a = 1;a = 2; 写一个变量之后,再写这个变量。 读后写 a = b;b = 1; 读一个变量之后,再写这个变量。上面三种情况,只要重排序两个操作的执行顺序,程序的执行结果将会被改变。

前面提到过,编译器和处理器可能会对操作做重排序。编译器和处理器在重排序时,会遵守数据依赖性,编译器和处理器不会改变存在数据依赖关系的两个操作的执行顺序。

注意,这里所说的数据依赖性仅针对单个处理器中执行的指令序列和单个线程中执行的操作,不同处理器之间和不同线程之间的数据依赖性不被编译器和处理器考虑。

as-if-serial 语义

as-if-serial 语义的意思指:不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。编译器,runtime 和处理器都必须遵守 as-if-serial 语义。

为了遵守 as-if-serial 语义,编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。但是,如果操作之间不存在数据依赖关系,这些操作可能被编译器和处理器重排序。为了具体说明,请看下面计算圆面积的代码示例:

复制代码
double pi = 3.14; //A
double r = 1.0; //B
double area = pi * r * r; //C

上面三个操作的数据依赖关系如下图所示:

如上图所示,A 和 C 之间存在数据依赖关系,同时 B 和 C 之间也存在数据依赖关系。因此在最终执行的指令序列中,C 不能被重排序到 A 和 B 的前面(C 排到 A 和 B 的前面,程序的结果将会被改变)。但 A 和 B 之间没有数据依赖关系,编译器和处理器可以重排序 A 和 B 之间的执行顺序。下图是该程序的两种执行顺序:

as-if-serial 语义把单线程程序保护了起来,遵守 as-if-serial 语义的编译器,runtime 和处理器共同为编写单线程程序的程序员创建了一个幻觉:单线程程序是按程序的顺序来执行的。as-if-serial 语义使单线程程序员无需担心重排序会干扰他们,也无需担心内存可见性问题。

程序顺序规则

根据 happens- before 的程序顺序规则,上面计算圆的面积的示例代码存在三个 happens- before 关系:

  1. A happens- before B;
  2. B happens- before C;
  3. A happens- before C;

这里的第 3 个 happens- before 关系,是根据 happens- before 的传递性推导出来的。

这里 A happens- before B,但实际执行时 B 却可以排在 A 之前执行(看上面的重排序后的执行顺序)。在第一章提到过,如果 A happens- before B,JMM 并不要求 A 一定要在 B 之前执行。JMM 仅仅要求前一个操作(执行的结果)对后一个操作可见,且前一个操作按顺序排在第二个操作之前。这里操作 A 的执行结果不需要对操作 B 可见;而且重排序操作 A 和操作 B 后的执行结果,与操作 A 和操作 B 按 happens- before 顺序执行的结果一致。在这种情况下,JMM 会认为这种重排序并不非法(not illegal),JMM 允许这种重排序。

在计算机中,软件技术和硬件技术有一个共同的目标:在不改变程序执行结果的前提下,尽可能的开发并行度。编译器和处理器遵从这一目标,从 happens- before 的定义我们可以看出,JMM 同样遵从这一目标。

重排序对多线程的影响

现在让我们来看看,重排序是否会改变多线程程序的执行结果。请看下面的示例代码:

复制代码
class ReorderExample {
int a = 0;
boolean flag = false;
public void writer() {
a = 1; //1
flag = true; //2
}
Public void reader() {
if (flag) { //3
int i = a * a; //4
……
}
}
}

flag 变量是个标记,用来标识变量 a 是否已被写入。这里假设有两个线程 A 和 B,A 首先执行 writer() 方法,随后 B 线程接着执行 reader() 方法。线程 B 在执行操作 4 时,能否看到线程 A 在操作 1 对共享变量 a 的写入?

答案是:不一定能看到。

由于操作 1 和操作 2 没有数据依赖关系,编译器和处理器可以对这两个操作重排序;同样,操作 3 和操作 4 没有数据依赖关系,编译器和处理器也可以对这两个操作重排序。让我们先来看看,当操作 1 和操作 2 重排序时,可能会产生什么效果?请看下面的程序执行时序图:

如上图所示,操作 1 和操作 2 做了重排序。程序执行时,线程 A 首先写标记变量 flag,随后线程 B 读这个变量。由于条件判断为真,线程 B 将读取变量 a。此时,变量 a 还根本没有被线程 A 写入,在这里多线程程序的语义被重排序破坏了!

※注:本文统一用红色的虚箭线表示错误的读操作,用绿色的虚箭线表示正确的读操作。

下面再让我们看看,当操作 3 和操作 4 重排序时会产生什么效果(借助这个重排序,可以顺便说明控制依赖性)。下面是操作 3 和操作 4 重排序后,程序的执行时序图:

在程序中,操作 3 和操作 4 存在控制依赖关系。当代码中存在控制依赖性时,会影响指令序列执行的并行度。为此,编译器和处理器会采用猜测(Speculation)执行来克服控制相关性对并行度的影响。以处理器的猜测执行为例,执行线程 B 的处理器可以提前读取并计算 a*a,然后把计算结果临时保存到一个名为重排序缓冲(reorder buffer ROB)的硬件缓存中。当接下来操作 3 的条件判断为真时,就把该计算结果写入变量 i 中。

从图中我们可以看出,猜测执行实质上对操作 3 和 4 做了重排序。重排序在这里破坏了多线程程序的语义!

在单线程程序中,对存在控制依赖的操作重排序,不会改变执行结果(这也是 as-if-serial 语义允许对存在控制依赖的操作做重排序的原因);但在多线程程序中,对存在控制依赖的操作重排序,可能会改变程序的执行结果。

参考文献

  1. Computer Architecture: A Quantitative Approach, 4th Edition
  2. Concurrent Programming on Windows
  3. Concurrent Programming in Java™: Design Principles and Pattern
  4. JSR-133: Java Memory Model and Thread Specification
  5. JSR 133 (Java Memory Model) FAQ

关于作者

程晓明,Java 软件工程师,国家认证的系统分析师、信息项目管理师。专注于并发编程,就职于富士通南大。个人邮箱: asst2003@163.com


感谢张龙对本文的审校。

给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ )或者腾讯微博( @InfoQ )关注我们,并与我们的编辑和其他读者朋友交流。

2013-01-26 03:1043298

评论

发布
暂无评论
发现更多内容

如何轻松快速构建区块链应用?技术大牛带来一线技术实践分享

腾源会

如何使用物联网低代码平台进行流程管理?

AIRIOT

低代码 物联网,

八大误区,逐个击破(终篇):云难以扩展、定制性差,还会让管理员失去控制权?

龙智—DevSecOps解决方案

Atlassian 云版 版本选择 迁移上云

SLSA: 成功SBOM的促进剂

安势信息

开源 开源软件供应链 软件物料清单 SBOM SLSA

RabbitMQ访问Web端口报错User can only log in via localhost

龙空白白

微信视频号如何用 PC 电脑做直播?

boshi

直播 视频号

区块哈希竞猜游戏系统开发(dapp)

薇電13242772558

哈希值

如何通过7个步骤编写出色的在线用户手册

小炮

学C++还是学Java?做软件研发还需掌握哪些知识和技能?

dvlinker

Java c++ 数据库 网络知识 汇编代码

JDBC 在性能测试中的应用

阿里巴巴云原生

阿里云 云原生 JDBC 压测

好用的人事管理软件有哪些?人事管理系统软件排名!

优秀

企业管理软件 OA管理系统

Helix QAC更新至2022.1版本,将持续提供高标准合规覆盖率

龙智—DevSecOps解决方案

C语言 静态代码分析 Helix QAC 代码合规率 代码合规

应用实践 | Apache Doris 整合 Iceberg + Flink CDC 构建实时湖仓一体的联邦查询分析架构

SelectDB

数据库 flink Doris iceberg

直播分享| 腾讯云 MongoDB 智能诊断及性能优化实践

MongoDB中文社区

mongodb

游戏资产复用:更快找到所需游戏资产的新方法

龙智—DevSecOps解决方案

游戏开发 游戏资产 艾尔登法环 游戏资产复用

在宇宙的眼眸下,如何正确地关心东数西算?

脑极体

火线沙龙第26期-多云安全专场

腾源会

混沌工程,了解一下

华为云开发者联盟

云计算 测试 后端 混沌工程 华为云

准备好迁移上云了?请收下这份迁移步骤清单

龙智—DevSecOps解决方案

迁移计划 迁移上云计划 迁移上云步骤 上云步骤清单 云迁移策略

直播回顾 | 云原生混部系统 Koordinator 架构详解(附完整PPT)

阿里巴巴云原生

阿里云 架构 云原生 混部 Koordinator

RabbitMQ基础知识

龙空白白

RabbitMQ

远程办公之:如何成为时间管理大师?| 社区征文

甜甜的白桃

初夏征文

小程序容器到底是什么

Geek_99967b

活动报名 | MongoDB 5.0 时序存储特性介绍

MongoDB中文社区

mongodb

Python 设计模式:适配器模式

宇宙之一粟

设计模式 适配器模式 6月月更

Fabric.js 手动加粗文本iText

德育处主任

canvas FabricJS 6月月更

Flutter中的GetX状态管理用起来真的那么香吗?

岛上码农

flutter ios 移动端开发 安卓开发 6月月更

为什么你的数据图谱分析图上只显示一个值?

清林情报分析师

数据分析 可视化 知识图谱 三元组 情报分析

脚本之美│VBS 入门交互实战

Windows Server 6月月更 VBS 脚本之美

openGauss Developer Day 2022正式开启,与开发者共建开源数据库根社区

开发协同,高效管理 | 社区征文

武师叔

初夏征文

深入理解Java内存模型(二)——重排序_Java_程晓明_InfoQ精选文章