1. 乱序问题

1.1. 乱序来源

  1. 编译器优化的重排序。编译器在 不改变单线程程序语义 的前提下,可以重新安排语句的执行顺序。
  2. 指令级并行的重排序。现代处理器采用了指令级并行技术(Instruction-Level Parallelism,ILP)来将多条指令重叠执行。如果不存在 数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
  3. 内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。

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 编译器将字节码编译为机器码,即动态编译期
%%
▶1.🏡⭐️◼️【🌈费曼无敌🌈⭐️第一步⭐️】◼️⭐️-point-20230409-0830%%
❕ ^yt5sch

如果加不加 volatile 生成的字节码文件都一个样,那在运行的时候 JVM 是怎么知道的呢?类属性在 JVM 中存储的时候会有一个属性:Access flags。JVM 在运行的时候就是通过该属性来判断操作的类属性有没有加 volatile 修饰,上图。

1.1.2. 指令执行乱序

并发基础-1、JMM与MESI

1.1.3. 内存写入乱序

CPU 架构中,因为 MESI 协议需要使用保存缓冲区,有可能产生不可见问题

1.2. Java volatile 乱序解决方案⭐️🔴

Java 中的 volatile 是通过禁止 即时编译优化内存写入乱序 来保证可见性的,其中

  1. 禁止 即时编译优化 是通过编译器屏障
  2. 禁止 内存写入乱序 是通过 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 的主要作用都是:在保证不改变程序运行结果的前提下,允许部分指令的重排序,最大限度的提升程序执行的效率。

2. 实战经验

3. 参考与感谢

https://www.jianshu.com/p/e6cda45d58d0

https://zhuanlan.zhihu.com/p/271701839