[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并发编程的艺术 作者:方腾飞;魏鹏;程晓明