在我们面试过程中,通常避免不了会被问到什么是指令重排序?本文就这个问题进行探索。

重排序

  • 前言
  • 一、重排序种类
  • 二、happens-before
  • 三、重排序
    • 1.数据依赖性
    • 2. as-if-serial语义
    • 3.程序顺序规则
    • 4.重排序在多线程中的影响

前言

在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排序。


一、重排序种类

在java语言中,重排序分为3种。

  1. 编译器优化的重排序。编译器在不改变单线程程序的语义前提下,可以重新安排语句的执行顺序。
  2. 指令级并行的重排序。现在处理器采用了指令集并行技术,来讲多条指令重叠执行。如果不存在依赖性,处理器可以改变语句对应的机器指令的执行顺序。
  3. 内存系统的重排序。由于处理器使用缓存和读写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。


上述1属于编译器重排序,2和3属于处理器重排序。这些重排序会导致多线程程序出现内存可见性问题。

对于编译器,JMM的编译器重排序规则会禁止特定类型的编译器重排序。

对于处理器重排序,JMM的处理器重排序规则会要求Java编译器在生成指令序列时,插入特定类型的内存屏障(Memory Barriers)指令,来禁止特定类型的处理器重排序。

内存屏障类型表

屏障类型 说明
LoadLoad Barriers 确保Load1数据的装在先于Load2及所有后续装载命令的装载
StoreStore Barriers 确保Store1数据刷新到内存先于Store2及后续所有后续存储指令
LoadStore Barriers 确保Load1数据装载先于Store2及所有后续存储指令刷新到内存
StoreLoad Barriers 确保Store1数据对其他处理器变得可见(刷新到内存)先于Load2以及所有后续装载指令。会使该屏障之前所有的内存访问指令完成后,才执行该屏障之后的内存访问指令

StoreLoad Barriers是一个“全能型”的屏障,它同时具有其他3个屏障的效果。现代的多处理器大多支持该屏障(其他类型的屏障不一定被所有处理器支持)。执行该屏障开销会很昂贵,因为当前处理器通常要把写缓冲区中的数据全部刷新到内存中(Buffer Fully Flush)。

二、happens-before

从JDK5开始,Java使用心得JSR-133内存模型,其中使用happen-before的概念来阐述操作之间的内存可见性。在JMM中如果一个操作的执行结果需要对另一个操作可见,那么两个操作之间必须要存在happends-before关系,这两个操作可以是在一个线程内,也可以在不同线程之间。

happens-before规则如下:

  1. 程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作。
  2. 监视器所规则:一个线程中的每个操作,happens-before于随后这个锁的加锁。
  3. volitaile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
  4. 传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。

JMM属于语言级的内存模型,它确保在不同编译器和不同处理平台上,通过禁止特定类型的编译器/处理器重排序,来保证一致的内存可见性
如下图所示,JMM基于不同的处理器编译器等做了不同的规则实现,来为程序员提供统一的happens-before规则。

三、重排序

重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段。

1.数据依赖性

两个操作访问同一个变量,且这两个操作其中有一个为写操作,那么这两个操作就存在数据依赖性。

写后读:写一个变量后,再读这个位置。

a=1;
b=a;

写后写:写一个变量后,再写这个变量。

a=1;
a=2;

读后写:读一个变量后,再写这个变量。

a=b;
b=1;

上面3种情况,只要重排序两个操作的执行顺序,程序的执行结果就会被改变。编译器和处理器不会改变存在数据依赖关系的两个操作的执行顺序。

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

2. as-if-serial语义

as-if-serial语义的意思是:不管怎么重排序,(单线程)程序的执行结果不能被改变。编译器、runtime和处理器都必须遵守as-if-serial语义。编译器和处理器不会对存在数据依赖关系的操作做重排序

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

在上述三个操作中,C同时和B、A存在数据依赖关系。因此再最终执行的指令序列中,C不能被重排序到A、B的前面。但A和B之间没有数据依赖关系,编译器和处理器可以对A和B的执行顺序进行重排序

A - B - C 。执行结果 c = 2;
B - A - C 。执行结果 c = 2;

遵守as-if-serial语义的编译器、runtime和处理器共同为编写单线程程序的程序员创建了一个幻觉:单线程程序是按程序的顺序来执行的。

as-if-serial语义使单线程程序员无需担心重排序会干扰他们,也无需担心内存可见性问题

3.程序顺序规则

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

上述代码中,根据happens-before的传递性推导,存在了三个happens-before关系。

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

这里虽然A happens-before B,但实际执行时B却可以排在A之前执行。这里JMM并不要求A一定要在B之前执行,因为这里操作A的结果不需要对操作B可见。并且无论A和B的执行顺序怎么变,最后的执行结果一致。JMM允许这种重排序,会认为这种重排序并不非法。

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

4.重排序在多线程中的影响

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

这里假设有两个线程A和B,A首先执行writer()方法,随后B线程接着执行reader()方法。
线程B在执行操作4时,能否看到线程A在操作1对共享变量a的写入呢?
答案是:不一定能看到。

由于操作1和操作2没有数据依赖关系,所以可以对这两个操作进行重排序。同样,操作3和操作4也可以进行重排序

情况一:操作1和操作2重排序

操作1和操作2做了重排序,程序执行时,线程A先修改了变量flag,随后线程B读取到这个变量,并作运算,此时变量a还没有被线程A写入。多线程环境下语义被重排序破坏了。

情况二:操作3和操作4重排序

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

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

Java并发编程之指令重排序相关推荐

  1. 【Java 并发编程】指令重排序规范 ( happens-before 先行发生原则 )

    文章目录 一.指令重排序规范 二.happens-before 先行发生原则 一.指令重排序规范 指令重排指的是 , 线程中如果两行代码 没有逻辑上的上下关系 , 可以对代码进行 重新排序 ; JVM ...

  2. Java并发编程实战————可重入内置锁

    引言 在<Java Concurrency in Practice>的加锁机制一节中作者提到: Java提供一种内置的锁机制来支持原子性:同步代码块."重入"意味着获取 ...

  3. Java 并发编程之可重入锁 ReentrantLock

    Java 提供了另外一个可重入锁,ReentrantLock,位于 java.util.concurrent.locks 包下,可以替代 synchronized,并且提供了比 synchronize ...

  4. Java并发编程-ReentrantLock可重入锁

    目录 1.ReentrantLock可重入锁概述 2.可重入 3.可打断 4.锁超时 5.公平锁 6.条件变量 Condition 1.ReentrantLock可重入锁概述 相对于 synchron ...

  5. JVM学习--(二)内存模型、可见性、指令重排序

    我们将根据JVM的内存模型探索java当中变量的可见性以及不同的java指令在并发时可能发生的指令重排序的情况. 内存模型 首先我们思考一下一个java线程要向另外一个线程进行通信,应该怎么做,我们再 ...

  6. 【Java并发编程】并发编程大合集

    转载请注明出处:http://blog.csdn.net/ns_code/article/details/17539599 为了方便各位网友学习以及方便自己复习之用,将Java并发编程系列内容系列内容 ...

  7. Java并发编程的学习

    转载出处:http://blog.csdn.net/ns_code/article/details/17539599 为了方便各位网友学习以及方便自己复习之用,将Java并发编程系列内容系列内容按照由 ...

  8. 【Java 并发编程】线程指令重排序问题 ( 指令重排序规范 | volatile 关键字禁止指令重排序 )

    文章目录 总结 一.指令重排序规范 二.指令重排序示例 总结 Java 并发的 333 特性 : 原子性 : 每个操作都是 不可拆分的原子操作 ; 在线程中进行 a++ 就不是原子操作 , 该操作分为 ...

  9. java排序为什么会出现多次排序结果不一样_并发理论基础:指令重排序问题

    为什么需要对指令进行重排序? 其实说到底都是源于对性能的优化,CPU运行效率 相比缓存.内存.硬盘IO之间效率有着指数级的差别,CPU作为系统的宝贵资源,那么如何更好的优化和利用这个资源就能提升整个计 ...

最新文章

  1. 干货回顾丨深度学习应用大盘点
  2. 初学Java Web(9)——学生管理系统(简易版)总结
  3. 哥德巴赫猜想(洛谷P1304题题解,Java语言描述)
  4. 编写Python高质量代码,资深程序员的 91 个建议
  5. NSOperation队列实实现多线程
  6. 渗透测试入门9之域渗透
  7. Spring Boot - 开发Web应用
  8. [原创]数据库视图介绍和使用
  9. 王道训练营Day24——Linked
  10. 淘宝客小程序制作(3)-API编写及部署
  11. 感悟《疯狂的程序员》
  12. 虽然不能去故宫办婚礼,但你可以帮故宫找“中纹”啊!
  13. 好趣艺术设计部落网页制作案例
  14. python用两分钟告诉你,怎样暴力破解隔壁老王的 WiFi 密码
  15. nmap的网络拓扑实现
  16. phy 驱动与 switch 驱动
  17. 51单片机倒计时计时器(计时结束闹钟)
  18. Dubbo源码分析(一):Dubbo源码的结构概述
  19. HTML中的<a>标签
  20. eth_clockgen.v

热门文章

  1. C# 人民币金额转大写
  2. cocos creator 动画编辑器以及骨骼动画的使用
  3. 使用Java代码将文件打包成RAR文件
  4. Qt +opencv 通过级联分类器训练的模型进行识别(车辆识别+人脸识别)
  5. 什么是前端什么是后端?前端后端区别
  6. 智能电表c语言开发,智能电表之自动抄表主流方案盘点(一)
  7. 计算机信息管理专业学ps吗,计算机信息管理专业个人技能怎么写
  8. Nutch的插件机制
  9. 一款车载GPS定位产品后端服务器架构的填坑之路(一)
  10. SGMII和SerDes的区别