指令重排序是编译器或处理器为了提高性能而做的优化。重排序不会给单线程带来内存可见性的问题,多线程中程序交错执行时,就可能造成内存可见性问题。
每一个指令都会包含多个步骤,每个步骤可能使用不同的硬件。因此,流水线技术产生了,它的原理是指令1还没有执行完,就可以开始执行指令2,而不用等到指令1执行结束之后再执行指令2,这样就大大提高了效率。
a = b + c;
d = e - f;
g = add(b, c);
如上代码中,先加载 b、c(注意,即有可能先加载 b,也有可能先加载 c),但是在执行 add(b, c) 的时候,需要等待 b、c 装载结束才能继续执行,也就是增加了停顿,那么后面的指令也会依次有停顿,这降低了计算机的执行效率。
为了减少这个停顿,我们可以先加载 e 和 f,然后再去加载 add(b, c),这样做对程序(串行)是没有影响的,但却减少了停顿。既然 add(b, c) 需要停顿,那还不如去做一些有意义的事情。
编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性(即后一个执行的语句无需依赖前面执行的语句的结果),处理器可以改变语句对应的机器指令的执行顺序。
由于处理器使用缓存和读写缓存冲区,这使得加载(load)和存储(store)操作看上去可能是在乱序执行,因为三级缓存的存在,导致内存与缓存的数据同步存在时间差。
写后读,写后写,读后写
as-if-serial 语义保证单线程内重排序后的执行结果和程序代码本身应有的结果是一致的,happens-before 关系保证正确同步的多线程程序的执行结果不被重排序改变。
无论如何重排序,程序执行的结果应该与代码顺序执行的结果一致(Java 编译器、运行时和处理器都会保证 Java 在单线程下遵循 as-if-serial 语义)。
如果操作 A happens-before 操作 B,那么操作 A 在内存上所做的操作对操作 B 都是可见的,不管它们在不在一个线程。
- 如果一个操作 happens-before 另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。
- 两个操作之间存在 happens-before 关系,并不意味着 Java 平台的具体实现必须要按照 happens-before 关系指定的顺序来执行。如果重排序之后的执行结果,与按 happens-before 关系来执行的结果一致,那么JMM也允许这样的重排序。
- 监视器锁规则:对一个锁的解锁,happens-before 于随后对这个锁的加锁。
- volatile 变量规则:对一个 volatile 域的写,happens-before 于任意后续对这个 volatile域的读。
- 传递性:如果 A happens-before B,且 B happens-before C,那么 A happens-before C。
- join 规则:如果线程 A 执行操作 ThreadB.join()并成功返回,那么线程 B 中的任意操作 happens-before 于线程 A 从 ThreadB.join() 操作成功返回。