并发编程专题-基础-11、乱序问题
1. 乱序问题
1.1. 乱序来源
- 编译器优化的重排序。编译器在 不改变单线程程序语义 的前提下,可以重新安排语句的执行顺序。
- 指令级并行的重排序。现代处理器采用了指令级并行技术(Instruction-Level Parallelism,ILP)来将多条指令重叠执行。如果不存在 数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
- 内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。
1 属于 编译器重排序,2 和 3 属于 处理器重排序。从 Java 源代码到最终实际执行的指令序列,会分别经历下面 3 种重排序:
1. 即时编译器优化
2. 指令执行乱序
3. 内存写入乱序
1.1.1. 即时编译优化
1.1.1.1. C 语言
volatile 和编译器屏障都是通过禁止 即时编译优化
来保证可见性的
编译器屏障
1.1.1.2. Java
编译期:像 c/c++ 只有一个编译期,就是调用 gcc 命令将 c/c++ 代码编译成汇编代码。但是 Java 中有两个编译期:
1、调用 javac 命令将 Java 代码编译成 Java 字节码,即静态编译期;
2、JIT 编译器将字节码编译为机器码,即动态编译期
❕ ^yt5sch
如果加不加 volatile 生成的字节码文件都一个样,那在运行的时候 JVM 是怎么知道的呢?类属性在 JVM 中存储的时候会有一个属性:Access flags。JVM 在运行的时候就是通过该属性来判断操作的类属性有没有加 volatile 修饰,上图。
1.1.2. 指令执行乱序
并发基础-1、JMM与MESI1.1.3. 内存写入乱序
CPU 架构中,因为 MESI 协议需要使用保存缓冲区,有可能产生不可见问题
1.2. Java volatile 乱序解决方案⭐️🔴
Java 中的 volatile 是通过禁止 即时编译优化
和 内存写入乱序
来保证可见性的,其中
- 禁止
即时编译优化
是通过编译器屏障 - 禁止
内存写入乱序
是通过 lock 前缀 (CPU 屏障)
x86 是可以保证不会出现 CPU 的指令执行乱序的
1.3. as-if-serial
[[为什么需用指令重排序 - 简书]]
as-if-serial 的意思是:不管指令怎么重排序,在单线程下执行结果不能被改变
不管是编译器级别还是处理器级别的重排序都必须遵循 as-if-serial 语义
为了遵守 as-if-serial 语义,编译器和处理器不会对存在 数据依赖关系 的操作做重排序。但是 as-if-serial 规则允许对 有控制依赖关系 的指令做重排序,因为在单线程程序中,对存在控制依赖的操作重排序,不会改变执行结果,但是多线程下确有可能会改变结果。
1.4. happens-before
A Happens-Before B:无论 A 事件和 B 事件是否发生在同一个线程里,A 发生过的事情对 B 来说总是可见的。
1.4.1. 程序顺序规则
一个线程中的每个操作,happens-before 于该线程中的任意后续操作。
主要含义是:在一个线程内不管指令怎么重排序,程序运行的结果都不会发生改变。和 as-if-serial 比较像。
1.4.2. 监视器锁规则-先加锁再解锁
对一个锁的解锁,happens-before 于随后对这个锁的加锁。
主要含义是:同一个锁的解锁一定发生在加锁之后
1.4.3. 管程锁定规则-后获锁可以感知前解锁线程的操作
一个线程获取到锁后,它能看到前一个获取到锁的线程所有的操作结果。
主要含义是:无论是在单线程环境还是多线程环境,对于同一个锁来说,一个线程对这个锁解锁之后,另一个线程获取了这个锁都能看到前一个线程的操作结果!(管程是一种通用的同步原语,synchronized 就是管程的实现)
1.4.4. volatile 变量规则
对一个 volatile 域的写,happens-before 于任意后续对这个 volatile 域的读。
主要含义是:如果一个线程先去写一个 volatile 变量,然后另一个线程又去读这个变量,那么这个写操作的结果一定对读的这个线程可见。
1.4.5. 传递性
如果 A happens-before B,且 B happens-before C,那么 A happens-before C。
1.4.6. start() 规则-子线程可见
如果线程 A 执行操作 ThreadB.start()(启动线程 B),那么 A 线程的 ThreadB.start() 操作 happens-before 于线程 B 中的任意操作。
主要含义是:线程 A 在启动子线程 B 之前对共享变量的修改结果对线程 B 可见。
1.4.7. join() 规则
如果线程 A 执行操作 ThreadB.join() 并成功返回,那么线程 B 中的任意操作 happens-before 于线程 A 从 ThreadB.join() 操作成功返回。
主要含义是:如果在线程 A 执行过程中调用了线程 B 的 join 方法,那么当 B 执行完成后,在线程 B 中所有操作结果对线程 A 可见。
1.4.8. 线程中断规则
对线程 interrupt 方法的调用 happens-before 于被中断线程的代码检测到中断事件的发生。
主要含义是:响应中断一定发生在发起中断之后。
1.4.9. 对象终结规则
就是一个对象的初始化的完成,也就是构造函数执行的结束一定 happens-before 它的 finalize() 方法。
一个 happens-before 规则对应于一个或多个编译器和处理器重排序规则。
as-if-serial 和 happens-before 的主要作用都是:在保证不改变程序运行结果的前提下,允许部分指令的重排序,最大限度的提升程序执行的效率。