在执行程序时,为了提供性能,处理器和编译器常常会对指令进行重排序,但是不能随意重排序,不是你想怎么排序就怎么排序,它需要满足以下两个条件:

  1. 在单线程环境下不能改变程序运行的结果;
  2. 存在数据依赖关系的不允许重排序

如果看过LZ上篇博客的就会知道,其实这两点可以归结于一点:无法通过happens-before原则推导出来的,JMM允许任意的排序。

as-if-serial语义

as-if-serial语义的意思是,所有的操作均可以为了优化而被重排序,但是你必须要保证重排序后执行的结果不能被改变,编译器、runtime、处理器都必须遵守as-if-serial语义。注意as-if-serial只保证单线程环境,多线程环境下无效。

下面我们用一个简单的示例来说明:

int a = 1 ;      //A
int b = 2 ; //B
int c = a + b; //C

A、B、C三个操作存在如下关系:A、B不存在数据依赖关系,A和C、B和C存在数据依赖关系,因此在进行重排序的时候,A、B可以随意排序,但是必须位于C的前面,执行顺序可以是A --> B --> C或者B --> A --> C。但是无论是何种执行顺序最终的结果C总是等于3。

as-if-serail语义把单线程程序保护起来了,它可以保证在重排序的前提下程序的最终结果始终都是一致的。

其实对于上段代码,他们存在这样的happen-before关系:

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

1、2是程序顺序次序规则,3是传递性。但是,不是说通过重排序,B可能会排在A之前执行么,为何还会存在存在A happens-beforeB呢?这里再次申明A happens-before B不是A一定会在B之前执行,而是A的对B可见,但是相对于这个程序A的执行结果不需要对B可见,且他们重排序后不会影响结果,所以JMM不会认为这种重排序非法。

我们需要明白这点:在不改变程序执行结果的前提下,尽可能提高程序的运行效率。

下面我们在看一段有意思的代码:

public class RecordExample1 {
public static void main(String[] args){
int a = 1;
int b = 2; try {
a = 3; //A
b = 1 / 0; //B
} catch (Exception e) { } finally {
System.out.println("a = " + a);
}
}
}

按照重排序的规则,操作A与操作B有可能会进行重排序,如果重排序了,B会抛出异常( / by zero),此时A语句一定会执行不到,那么a还会等于3么?如果按照as-if-serial原则它就改变了程序的结果。其实JVM对异常做了一种特殊的处理,为了保证as-if-serial语义,Java异常处理机制对重排序做了一种特殊的处理:JIT在重排序时会在catch语句中插入错误代偿代码(a = 3),这样做虽然会导致cathc里面的逻辑变得复杂,但是JIT优化原则是:尽可能地优化程序正常运行下的逻辑,哪怕以catch块逻辑变得复杂为代价。

重排序对多线程的影响

在单线程环境下由于as-if-serial语义,重排序无法影响最终的结果,但是对于多线程环境呢?

如下代码(volatile的经典用法):

public class RecordExample2 {
int a = 0;
boolean flag = false; /**
* A线程执行
*/
public void writer(){
a = 1; // 1
flag = true; // 2
} /**
* B线程执行
*/
public void read(){
if(flag){ // 3
int i = a + a; // 4
}
} }

A线程执行writer(),线程B执行read(),线程B在执行时能否读到 a = 1 呢?答案是不一定(注:X86CPU不支持写写重排序,如果是在x86上面操作,这个一定会是a=1,LZ搞了好久都没有测试出来,最后查资料才发现)。

由于操作1 和操作2 之间没有数据依赖性,所以可以进行重排序处理,操作3 和操作4 之间也没有数据依赖性,他们亦可以进行重排序,但是操作3 和操作4 之间存在控制依赖性。假如操作1 和操作2 之间重排序:

按照这种执行顺序线程B肯定读不到线程A设置的a值,在这里多线程的语义就已经被重排序破坏了。

操作3 和操作4 之间也可以重排序,这里就不阐述了。但是他们之间存在一个控制依赖的关系,因为只有操作3 成立操作4 才会执行。当代码中存在控制依赖性时,会影响指令序列的执行的并行度,所以编译器和处理器会采用猜测执行来克服控制依赖对并行度的影响。假如操作3 和操作4重排序了,操作4 先执行,则先会把计算结果临时保存到重排序缓冲中,当操作3 为真时才会将计算结果写入变量i中

通过上面的分析,重排序不会影响单线程环境的执行结果,但是会破坏多线程的执行语义

参考资料

  1. 周志明 :《深入理解Java虚拟机》
  2. 方腾飞:《Java并发编程的艺术》

【死磕Java并发】-----Java内存模型之重排序的更多相关文章

  1. Java内存模型_重排序

    重排序:是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段 1..编译器优化的重排序.编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序. 2..指令级并行的重排序.现 ...

  2. Java并发(三):重排序

    在执行程序时为了提高性能,提高并行度,编译器和处理器常常会对指令做重排序.重排序分三种类型: 编译器优化的重排序.编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序. 指令级并行的重排序 ...

  3. 【死磕Java并发】-----内存模型之happens-before

    在上篇博客([死磕Java并发]-----深入分析volatile的实现原理)LZ提到过由于存在线程本地内存和主内存的原因,再加上重排序,会导致多线程环境下存在可见性的问题.那么我们正确使用同步.锁的 ...

  4. 《java并发编程实战》读书笔记13--Java内存模型,重排序,Happens-Before

    第16章 Java内存模型 终于看到这本书的最后一章了,嘿嘿,以后把这本书的英文版再翻翻.这本书中尽可能回避了java内存模型(JMM)的底层细节,而将重点放在一些高层设计问题,例如安全发布,同步策略 ...

  5. java并发学习--第九章 指令重排序

    一.happns-before happns-before是学习指令重排序前的一个必须了解的知识点,他的作用主要是就是用来判断代码的执行顺序. 1.定义 happens-before是用来指定两个操作 ...

  6. Java内存模型之重排序

    参考链接:https://blog.csdn.net/huzhigenlaohu/article/details/51595676

  7. Java并发编程的艺术(二)——重排序

    当我们写一个单线程程序时,总以为计算机会一行行地运行代码,然而事实并非如此. 什么是重排序? 重排序指的是编译器.处理器在不改变程序执行结果的前提下,重新排列指令的执行顺序,以达到最佳的运行效率. 重 ...

  8. 关于JAVA中的static方法、并发问题以及JAVA运行时内存模型

    一.前言 最近在工作上用到了一个静态方法,跟同事交流的时候,被一个问题给问倒了,只怪基础不扎实... 问题大致是这样的,“在多线程环境下,静态方法中的局部变量会不会被其它线程给污染掉?”: 我当时的想 ...

  9. Java多线程时内存模型

    1. 概述 多任务和高并发是衡量一台计算机处理器的能力重要指标之一.一般衡量一个服务器性能的高低好坏,使用每秒事务处理数(Transactions Per Second,TPS)这个指标比较能说明问题 ...

随机推荐

  1. 出现NoClassDefFoundError,始终无法引入jar的解决

    在拉取代码后,项目的部分版本与本地存在的不一定一致,所以IDEA会自动下载并引入,但是在启动时可能存在java.lang.NoClassDefFoundError这个报错 比如引入marshallin ...

  2. A Child's History of England.4

    Still, the Britons would not yield. They rose again and again, and died by thousands, sword in hand. ...

  3. Hadoop【Hadoop-HA搭建(HDFS、YARN)】

    目录 0.HDFS-HA的工作机制 1. HDFS-HA集群配置 1.1 环境准备 1.2 规划集群 1.3 配置Zookeeper集群 2. 配置HDFS-HA集群 3. 启动HDFS-HA集群 4 ...

  4. ios加载html5 audio标签用js无法自动播放

    html5 audio标签在ios 微信浏览器中是无法自动播放的,最近在做一个小的项目遇到这个问题,安卓和pc都是正常的,唯独ios不行,查阅了很多资料,找到了以下方法,也许不是最好用的方法,如果有更 ...

  5. Angular中怎样创建service服务来实现组件之间调用公共方法

    Angular组件之间不能互相调用方法,但是可以通过创建服务来实现公共方法的调用. 实现 创建服务命令 ng g service 服务路径/服务名 比如这里在app/services目录下创建stor ...

  6. 案例分析 CAN OPEN 调试记录 进度

    2020.12.29 发现一片博客:https://blog.csdn.net/harrycomeon/article/details/94650103 需要一个硬件:CAN分析仪,网上200元左右. ...

  7. C++构造函数和析构函数初步认识

    构造函数 1.构造函数与类名相同,是特殊的公有成员函数.2.构造函数无函数返回类型说明,实际上构造函数是有返回值的,其返回值类型即为构造函数所构建到的对象.3.当新对象被建立时,构造函数便被自动调用, ...

  8. 100个Shell脚本——【脚本9】统计ip

    [脚本9]统计ip 有一个日志文件,日志片段:如下: 112.111.12.248 – [25/Sep/2013:16:08:31 +0800]formula-x.haotui.com "/ ...

  9. 【AWS】【Basis】基础概念

    1.基础服务类型: 1.1. 链接: 官方文档,很详细:https://www.amazonaws.cn/products/#compute_networking/?nc1=f_dr 这个是一个whi ...

  10. EntityFramework Core (一)记一次 .net core 使用 ef 6

    使用传统的sql去操作数据库虽然思路更加清晰,对每一步数据库读写操作都能监控到,但是对大数据存储,或存储规则复杂的程序就需要编写大量的SQL语句且不易维护..orm大大方便了复杂的数据库读写操作, 让 ...