重排序
什么是重排序?
计算机在执行程序时,为了提高执行效率,编译器和处理器会对指令进行重排。
为什么重排序可提高性能?
现代CPU使用流水线技术,多个指令可以同时执行。
例如下面的代码
a = b + c;
d = e - f;
先加载 b,c 但在执行 b + c 的时候,需要等待 b,c 加载完成,也就也是增加了停顿,后面的指令在等待 b + c 完成。
为了减少停顿,我们可以先加载 e,f 然后再加载 b + c。这对程序执行没有影响,却提高了效率。
指令重排带了了乱序的问题,但大大提高了CPU的执行效率。
指令重排一般有三种:
- 编译器优化重排
不改单线程程序语义的情况下,重新安排指令的执行顺序。
- CPU指令并行重排
如果指令间不存在数据依赖(后面的语句依赖前面的结果),CPU可以改变机器指令的执行顺序。
- 内存系统重排
因为CPU使用缓存,是的 load 和 store 操作看上去在乱序执行,因为三级缓存的存在,导致内存和缓存的同步存在时间差。
指令重排可以保证串行语义一致,但没有义务保证多线程的语义也一致。
顺序一致性
顺序一致性:
- 一个线程中所有操作必须按照程序的顺序来执行
- 不管程序是否同步,所有线程只能看到一个单一的执行顺序。即每个操作是原子的,并立刻对所有线程可见。
JMM 并没有提供这样保证。
JMM 顺序一致性
同步程序
JMM 中,临界区内的代码可以发生重排序。
JMM 会在进入和退出临界区做特殊处理,使得在临界区内程序获得与顺序一致性模型相同的内存视图。
JMM的具体实现方针是:在不改变(正确同步的)程序执行结果的前提下,尽量为编译期和处理器的优化打开方便之门。
未同步的程序
JMM 提供最小安全性:
线程读到的值
- 要么是默认值
- 要么是之前某个线程写入的值
JMM 不保证 未同步程序 执行结果与该程序顺序一致性执行结果一致。
未同步程序在 JMM 和 顺序一致性 执行差异:
顺序一致性 | JMM | |
---|---|---|
单线程 | 按照程序顺序执行 | 不保证按照程序顺序执行,但保证重排序不影响结果。 |
操作执行顺序 | 保证所有线程看到一致的顺序 | 不保证所有线程看到一致的执行顺序(JMM 不保证所有操作立即可见) |
内存 | 内存读写原子性 | 不保证 64 位的 long 和 double 类型写操作原子性。 |
happens-before
什么是 haapens-before
一方面,程序员需要 JMM 提供一个强的内存模型来编写代码;另一方面,编译器和处理器希望 JMM 的束缚越少越好,这样它们可以做尽可能多的优化,希望有一个弱的内存模型。
JMM 考虑了这两种需求,找到了平衡点,对编译器和处理器来说:只要不改变程序的执行结果(单线程程序和正确同步了的多线程程序),编译器和处理器怎么优化都行。
对于程序员,JMM 提供了 happens-before规则(JSR-133规范),简单易懂,并提供了足够强的内存可见性保证。
JMM 使用 happens-before 的概念来定制两个操作间的执行顺序。这两个操作可以在一个线程以内,也可以是不同的线程之间。因此,JMM可以通过happens-before关系向程序员提供跨线程的内存可见性保证。
happens-before 定义:
- 如果一个操作 happens-before 另一个操作,第一个操作执行结果对第二个操作可见,且第一个操作操作的执行顺序排在第二个操作之前。
- 两个操作存在 happens-before 关系,并不意味着 Java 平台的具体实现按照 happens-before 指定顺序执行。在保证结果一致的前提下,JMM 也运行重排序。
简而言之,如果 A happens-before B, 那么 A 在内存上所做的操作对于 B 都是可见的,不管它们在不在一个线程中。
天然的 happens-before 关系
- 程序顺序:一个线程中每一个操作,happens-before 该线程中任意后续操作
- 监视器锁: 一个锁的解锁,happens-before 随后对改锁的加锁
- volatile:对一个 volatile 域的写,happens-before 于任意后续对这个 volatile 域的读
- 传递性: A happens-before B, B happens-before C, 那么 A happens-before C
- start: 线程 A 执行 threadB.start() , threadB.start() 操作 happens-before 于 B的任意操作
- join: 线程 A 执行 threadB.join(), 线程B的任意操作 happens-before 于线程A从 threadB.join()的返回。