深入理解JMM(Java内存模型) --(二)重排序
[转载自并发编程网 – ifeve.com 原文链接:http://ifeve.com/tag/jmm/]
数据依赖性
如果两个操作访问同一个变量,且这两个操作中有一个为写操作,此时这两个操作之间就存在数据依赖性。数据依赖分下列三种类型:
名称 | 代码示例 | 说明 |
写后读 | 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关系:
- A happens- before B;
- B happens- before C;
- 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语义允许对存在控制依赖的操作做重排序的原因);但在多线程程序中,对存在控制依赖的操作重排序,可能会改变程序的执行结果。
参考文献
- Computer Architecture: A Quantitative Approach, 4th Edition
- Concurrent Programming on Windows
- Concurrent Programming in Java™: Design Principles and Pattern
- JSR-133: Java Memory Model and Thread Specification
- JSR 133 (Java Memory Model) FAQ
深入理解JMM(Java内存模型) --(二)重排序的更多相关文章
- Java内存模型_重排序
重排序:是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段 1..编译器优化的重排序.编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序. 2..指令级并行的重排序.现 ...
- 【死磕Java并发】-----Java内存模型之重排序
在执行程序时,为了提供性能,处理器和编译器常常会对指令进行重排序,但是不能随意重排序,不是你想怎么排序就怎么排序,它需要满足以下两个条件: 在单线程环境下不能改变程序运行的结果: 存在数据依赖关系的不 ...
- Java内存模型之重排序
参考链接:https://blog.csdn.net/huzhigenlaohu/article/details/51595676
- Java多线程专题2: JMM(Java内存模型)
合集目录 Java多线程专题2: JMM(Java内存模型) Java中Synchronized关键字的内存语义是什么? If two or more threads share an object, ...
- 深入理解JVM - Java内存模型与线程 - 第十二章
Java内存模型 主内存与工作内存 Java内存模型主要目标:定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节.此处的变量(Variable)与Java编程中 ...
- Java并发编程:JMM(Java内存模型)和volatile
1. 并发编程的3个概念 并发编程时,要想并发程序正确地执行,必须要保证原子性.可见性和有序性.只要有一个没有被保证,就有可能会导致程序运行不正确. 1.1. 原子性 原子性:即一个或多个操作要么全部 ...
- Java并发编程:JMM (Java内存模型) 以及与volatile关键字详解
目录 计算机系统的一致性 Java内存模型 内存模型的3个重要特征 原子性 可见性 有序性 指令重排序 volatile关键字 保证可见性和防止指令重排 不能保证原子性 计算机系统的一致性 在现代计算 ...
- Java 线程 — JMM Java内存模型
JMM Java Memory Model,Java内存模型,属于语言级的内存模型 并发编程中存在的问题: 如何通信:用于线程之间交换信息.两种方式:共享内存,消息传递 如何同步:用于控制不同线程间操 ...
- Java内存模型(二)
volatile型变量的特殊规则 volatile是Java虚拟机提供的最轻量级的同步机制,当一个变量被定义成volatile后,它将具备两种特性,第一是保证此变量对所有线程的可见性,这里的“可见性” ...
- 《java并发编程实战》读书笔记13--Java内存模型,重排序,Happens-Before
第16章 Java内存模型 终于看到这本书的最后一章了,嘿嘿,以后把这本书的英文版再翻翻.这本书中尽可能回避了java内存模型(JMM)的底层细节,而将重点放在一些高层设计问题,例如安全发布,同步策略 ...
随机推荐
- Oracle创建 表空间 用户 给用户授权命令
//创建表空间 create tablespace ACHARTSdatafile 'D:\oradata\orcl\ACHARTS01.DBF' size 800mautoextend on nex ...
- UVa 11998 破碎的键盘(数组实现链表)
题意: 输入一行字符,其中包含'[' 和 ‘]’, 意思为键盘上的home 和 end 键, 然后模拟字符在键盘上输入. 输入一行最终的结果 分析: 用数组模拟一个链表, 在链表的头尾插入字母然后输出 ...
- 一个IT工薪族的4年奋斗成果
关于标题:为了方便传播,使用了"最简化"的一段. 过段时间,考虑改为"大学毕业4年-回顾和总结(11):一个IT工薪族的4年奋斗成果(2012年6月17日~2016年6 ...
- Java异常使用要点记录
近期有离职的想法,奈何简历过于寒碜,技术懂的少,基础也薄弱,想要提升自己却不知从哪里入手.在郁闷一段时间后偶然间看到一篇博客,博主说出了许多人的心声,同时也指出了切入点,的确基础才是重点,是时候沉迷学 ...
- hadoop_exporter
1.下载安装go 1.下载二进制包:go1.4.linux-amd64.tar.gz. 2.将下载的二进制包解压至 /usr/local目录. tar -C /usr/local -xzf go1.4 ...
- jQuery实现上传进度条效果
效果:(点击上传按钮) See the Pen pjGNJr by moyu (@MoYu1991) on CodePen. html代码: <!DOCTYPE html> <h ...
- SPOJ 3267 D-query (可持久化线段树,区间重复元素个数)
D-query Given a sequence of n numbers a1, a2, ..., an and a number of d-queries. A d-query is a pair ...
- node.js 读取文件--createReadStream
createReadStream 是fs模块里面读流的一个方法 这个方法基于fs模块的,所以我们先要引进fs模块 let fs=require("fs"); createReadS ...
- MyChrome制作Chrome浏览器便携版
Google Chrome官方离线下载地址: https://api.shuax.com/tools/getchrome MyChrome下载地址: http://code.taobao.org/p/ ...
- 51nod1020 逆序排列
t<=10000个问,每次问n<=1000的全排列中逆序数对为k<=10000个的有多少,mod 1e9+7. 直接dp,$f(i,j)$--i的全排列中逆序数对为j的有多少,$f( ...