写点什么

深入理解 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:1043538

评论

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

转转商品系统高并发实战(数据篇)

转转技术团队

分布式 高并发

左益豪:用代码创造一个新世界|OneFlow U

OneFlow

实习 社区之星

Spring(五、注解开发)

开源 8月月更

Dapr在Java中的实践 之 环境准备

万猫学社

微服务 dapr Sidecar

RT-Thread记录(三、RT-Thread 线程操作函数及线程管理与FreeRTOS的比较)

矜辰所致

RTT RT-Thread 8月月更 线程操作

开源一夏 | 使用 JavaScript 和 CSS 做一个图片转 PDF 的转换器

海拥(haiyong.site)

JavaScript 开源 前端 8月月更

分分钟快速定制您的专属个性化软件应用——BizTool自动化工具简介

BizFree

软件开发 快速开发 低代码开发 个性化 应用开发

鲲鹏开发者创享日2022:鲲鹏全栈创新 与开发者共建数字湖南

科技热闻

结合“xPlus”探讨软件架构的创新与变革

BizFree

敏捷开发 软件架构 数字化 信息化 软件定制

Java 在Word中合并单元格时删除重复值

在下毛毛雨

java; 合并单元格 删除重复值

STM32的内存管理相关(内存架构,内存管理,map文件分析)

矜辰所致

内存 stm32 Flash 8月月更

写给 Java 程序员的前端 Promise 教程

江南一点雨

Java spring 前端 springboot Promise

海外邮件发送指南(一)

极光GPTBots-极光推送

消息推送 邮件 SendCloud

【LeetCode】受限条件下可到达节点的数目Java题解

Albert

LeetCode 8月月更

在SAP分析云里根据业务数据绘制词云(Word Cloud)

汪子熙

SaaS SAP 词云 8月月更 word-cloud

自动驾驶中的SLAM

博文视点Broadview

以技术御风险,护航云原生 | 同创永益 X 博云举办产品联合发布会

BoCloud博云

云计算 容器 云原生

Kubernetes资源编排系列之四: CRD+Operator篇

阿里云大数据AI技术

大数据 运维

Dapr在Java中的实践 之 服务调用

万猫学社

微服务 dapr Sidecar

云原生系列五:Kafka 集群数据迁移基于Kubernetes的内部

叶秋学长

kafka 开源 Kubernetes 8月月更

一名合格的程序员是如何优雅地解决线上问题的?

程序员小毕

Java 程序员 架构 程序人生 后端

如何用建木CI构建前端E2E质量自查

Jianmu

DevOps 前端 持续集成 代码质量 自动化测试

K8S之Flannel的vxlan网络模式初步源码解析

k8s flannel 签约计划第三季

一文读懂配置管理(CM)

SEAL安全

企业安全 企业it安全 代码安全

开源一夏 | 牛plus,多层嵌套动态JSON该如何解析总结

知识浅谈

开源 8月月更

业务缓存之体系化设计与开发

Qunar技术沙龙

系统开发

PWA 应用 Service Worker 缓存的一些可选策略和使用场景

汪子熙

typescript 前端开发 angular Service Worker 8月月更

易周金融分析 | 互联网系小贷平台密集增资;上半年银行理财子公司综合评价指数发布

易观分析

金融 分析 易周金融

使用类似搭积木的低代码开发方式进行 SAP API 开发

汪子熙

低代码 云平台 SAP 8月月更 low-code

Dapr在Java中的实践 之 状态管理

万猫学社

Java 微服务 dapr Sidecar

5S软件就是将软件应用全维度简单化的软件系统

BizFree

k8s 敏捷开发 软件架构 高性能 快捷调试

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