什么是指令重排

[Q&A] 什么是指令重排?

Person person= new Person(); 这段代码其实是分为三步执行:
1、为 person 分配内存空间
2、初始化 person
3、将 person 指向分配的内存地址
我们理解的执行顺序应该为: 1(分配空间) → 2(初始化) → 3(指向分配的地址)
由于 JVM 具有指令重排的特性,实际执行顺序有可能变成:1(分配空间) → 3(指向分配的地址) → 2(初始化)


[Q&A] 指令重排的目的
在执行程序时,为了提高性能


[Q&A] 指令重排可能引起的问题? 问题场景1
1、因为 as-if-serial语义,指令重排在单线程环境下不会出现问题。

单线程执行时序图
在这里插入图片描述

根据《The Java Language Specification,Java SE 7 Edition》(后文简称为Java语言规范),所有线程在执行Java程序时必须要遵守intra-thread semantics。intra-thread semantics保证重排序不会改变单线程内的程序执行结果。换句话说,intra-thread semantics允许那些在单线程内,不会改变单线程程序执行结果的重排序
上面3行伪代码的2和3之间虽然被重排序了,但这个重排序并不会违反intra-thread semantics。这个重排序在没有改变单线程程序执行结果的前提下,可以提高程序的执行性能。只要保证2排在4的前面,即使2和3之间重排序了,也不会违反intra-thread semantics

2、但是在多线程环境下会导致一个线程获得还没有初始化的实例。
例如,线程T1 执行了 1(分配空间) → 3(指向分配的地址),此时 线程T2 调用 getPerson() 后发现 person 不为空,因此返回 person,但此时 person 还未被初始化,就引入了脏数据。

Instance instance=new Singleton(); 创建了一个对象。这一行代码可以分解为如下的3行伪代码。
memory = allocate();  // 1:分配对象的内存空间
ctorInstance(memory);  // 2:初始化对象
instance = memory;   // 3:设置instance指向刚分配的内存地址
上面3行伪代码中的2和3之间,可能会被重排序。2和3之间重排序之后的执行时序如下。
memory = allocate();  // 1:分配对象的内存空间
instance = memory;   // 3:设置instance指向刚分配的内存地址 。注意,此时对象还没有被初始化!
ctorInstance(memory);  // 2:初始化对象

多线程执行时序图 在这里插入图片描述
由于单线程内要遵守intra-thread semantics,从而能保证A线程的执行结果不会被改变。但是,B线程将看到一个还没有被初始化的对象。 重排序引发多线程错误的分析,这是个场景
-----------------------------------------------------------------------------读书笔记摘自 书名:Java并发编程的艺术 作者:方腾飞;魏鹏;程晓明


[Q&A] 重排序对多线程的影响 问题场景2

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
               ……
           }
       }
}

假设有两个线程A和B,A首先执行writer()方法,随后B线程接着执行reader()方法

由于操作1和操作2没有数据依赖关系,编译器和处理器可以对这两个操作重排序;操作1和操作2做了重排序。程序执行时,线程A首先写标记变量flag,随后线程B读这个变量。由于条件判断为真,线程B将读取变量a。此时,变量a还没有被线程A写入,在这里多线程程序的语义被重排序破坏了!
操作3和操作4没有数据依赖关系,编译器和处理器也可以对这两个操作重排序。在程序中,操作3和操作4存在控制依赖关系重排序后,线程B先计算a*a,此时线程A还没读到a的值,所有后续赋值肯定是不准确的。重排序在这里破坏了多线程程序的语义!

[Q&A] 指令重排分类 并行执行这几个字有点眼前一亮,2个人干一个人的事指定快
从Java源代码到最终实际执行的指令序列,会分别经历下面3种重排序。
源代码 → 编译器优化的重排序 → 指令级并行的重排序 → 内存系统的重排序 → 最终执行的指令序列

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

上述的1属于编译器重排序,2和3属于处理器重排序。


[Q&A] 如何规避多线程程序出现内存可见性问题?
1、对于编译器,JMM的编译器重排序规则会禁止特定类型的编译器重排序(不是所有的编译器重排序都要禁止)。
2、对于处理器,通过内存屏障指令来禁止特定类型的处理器重排序。


[Q&A] 程序顺序规则
在计算机中,软件技术和硬件技术有一个共同的目标:
在不改变程序执行结果的前提下,尽可能提高并行度。编译器处理器 遵从这一目标, JMM 同样遵从这一目标。

JMM对这两种不同性质的重排序,采取了不同的策略
对于会改变程序执行结果的重排序,JMM要求编译器和处理器必须禁止这种重排序。
对于不会改变程序执行结果的重排序,JMM对编译器和处理器不做要求(JMM允许这种重排序)。

-----------------------------------------------------------------------------摘自 书名:Java并发编程的艺术 作者:方腾飞;魏鹏;程晓明

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值