JAVA多线程系列--指令重排和happens-before规则

1 背景

  我们在平时所习惯的单线程编程中默认了一种乐观的模型——串行一致性。即在程序中只存在唯一的操作执行顺序,并且在每次读取变量时,都能获得在执行序列(任何处理器)最近一次写入该变量的值。但在JVM底层的很多操作在现代多处理器架构中都不会提供这种串行一致性。这样是导致线程不完全问题的深层次原因。

  导致非串行执行的原因是指令重排引起的,下面笔者将介绍下指令重排和规避指令重排的方法happens-before规则

2 指令重排

2.1 指令重排是什么

  Java 语言规范规定了JVM线程内部维持顺序化语义,也就是说只要程序的最终结果等同于它在严格的顺序化环境下的结果,那么指令的执行顺序就可能与代码的顺序不一致。这个过程通过叫做指令的重排序。指令重排序存在的意义在于:JVM能够根据处理器的特性(CPU的多级缓存系统、多核处理器等)适当的重新排序机器指令,使机器指令更符合CPU的执行特点,最大限度的发挥机器的性能。

2.2 重排序的背景

  我们知道现代CPU的主频越来越高,与cache的交互次数也越来越多。当CPU的计算速度远远超过访问cache时,会产生cache wait,过多的cache wait就会造成性能瓶颈。
  针对这种情况,多数架构(包括X86)采用了一种将cache分片的解决方案,即将一块cache划分成互不关联地多个 slots (逻辑存储单元,又名 Memory Bank 或 Cache Bank),CPU可以自行选择在多个 idle bank 中进行存取。这种 SMP 的设计,显著提高了CPU的并行处理能力,也回避了cache访问瓶颈。

2.3 重排序的种类

  1. 编译期重排
    编译源代码时,编译器依据对上下文的分析,对指令进行重排序,以之更适合于CPU的并行执行。
  2. 运行期重排
    CPU在执行过程中,动态分析依赖部件的效能,对指令做重排序优化。

3 happens-before规则

   Java存储模型有一个happens-before原则,就是如果动作B要看到动作A的执行结果(无论A/B是否在同一个线程里面执行),那么A/B就需要满足happens-before关系。

1)同一个线程中的每个Action都happens-before于出现在其后的任何一个Action。

(2)对一个监视器的解锁happens-before于每一个后续对同一个监视器的加锁。

(3)对volatile字段的写入操作happens-before于每一个后续的同一个字段的读操作。

(4Thread.start()的调用会happens-before于启动线程里面的动作。

(5Thread中的所有动作都happens-before于其他线程检查到此线程结束或者Thread.join()中返回或者Thread.isAlive()==false。

(6)一个线程A调用另一个另一个线程B的interrupt()都happens-before于线程A发现B被A中断(B抛出异常或者A检测到B的isInterrupted()或者interrupted())。

(7)一个对象构造函数的结束happens-before与该对象的finalizer的开始

(8)如果A动作happens-before于B动作,而B动作happens-before与C动作,那么A动作happens-before于C动作。

  以上原则归结为一点:happens-before规则是用来判断一个动作对另一个动作是否可见的法则

4 总结

  在真实环境下,如果动作A和动作B的执行顺序是可以通过指令重排发送变化的,但是你需要保证A和B的可见性,此时用户变可以用满足happens-before原则的操作来规避指令重排。比如用Java多线程工具类,如volatile,lock等。

猜你喜欢

转载自blog.csdn.net/niyuelin1990/article/details/78803594