可见性和有序性
数据的可见性、原子性、有序性是并发编程的主要问题,Java 通过 CAS 操作解决了并发编程中的原子性问题
CPU 物理缓存结构
高数缓存:由于 CPU 的存取速度比内存的存取速度快,所以在内存和 CPU 之间是由高数缓存提高存取的速率。高数缓存中也分为 L3、L2、L1 缓存,通过高速缓存可以保证指令流水线持续运行,可以避免 CPU 停顿下来的等待向内存写入数据的问题。通过批处理方式刷新写入缓存区、以及合并缓冲区中对同一内存地址的多次读写,减少对内存总线的占用
L1 缓存:接近 CPU、容量小、存取速度快、在每一个 CPU 核心上都有一个 L1 缓存
L2 缓存:是在 L1 上的缓存,容量比 L1 大、速度低一点、在一般情况下都有一个独立的 L2 缓存
L3 缓存:接近主存、容量最大、速度最低、同一个 CPU 核心上不同 CPU 内核共享
并发编程的存在的问题
- 原子性:是对于一个操作,不可中断的一个和一系列操作。是指不会被线程的调度打断的操作。从开始到结束不会被任何线程切换
- 可见性:一个线程对共享变量的修改,能对另一个线程能够立刻可见。在 JAVA 会将所有的变量都放在公共主存中,当一个线程对其进行修改的时候会将其变量复制到自己的工作内存(私有内存)中,对其进行的操作是对于自己内存中的数据进行操作然后将其写入到主内存中
- 有序性:程序的有序性是指代码的运行流程和编码流程一样,但是在 JAVA 中由于优化的原因实际执行顺序和编码顺序不同
硬件层的 MESI 协议原理
由于现代的 CPU 存在多种高速缓存,由于 L1 和 L2 是每一个 CPU 核心都有的,L3 是所有核心共有的。所以 CPU 处理的流程大致是:将计算所需要的数据缓存到高速缓存中,在 CPU 进行计算后将数据写会到高速缓存中,然后在运算过程完成后将数据同步到内存中。由于运算都在不同的 CPU 核心中,由于 CPU 核心都是操作高速缓存中数据。就可能看到同一个变量的缓存值都不一样就会发送内存的可见性问题
总线锁和缓存锁
解决内存的可见性问题 CPU 提供了两种解决方法:总线锁和缓存锁
总线锁
操作系统提供了总线锁的机制。前端总线(CPU 总线)。是所有 CPU 与芯片组连接的主干道,负责 CPU 与外界所有的部件的通信。包括高速缓存、内存、北桥、控制总线向各个部件发送控制信号,通过地址总线发送地址信号指定其要访问的部件、通过数据总线进行双向传输
是 CPU 核心操作数据时将总线上发出 LOCK# 信号锁住内存变量所在的缓存行,就会阻塞其他 CPU 核心的操作,使操作的 CPU 独占共享内存。就会使得其他 CPU 核心不能操作其他主存的地址数据,开销比较大
存在的缺陷:某一个 CPU 访问主存时,总线锁把 CPU 和主存的通信给锁住了,其他 CPU 不能操作其他主存地址的数据,效率低下开销大
缓存锁
缓存锁降低了锁的粒度。为了达到数据的访问的一致性,需要在高速缓存中遵循一些协议,存取数据根据协议进行操作,常见的协议 MSI、MESI、MOSI 最常见的缓存一致性协议就是 MESI
是对于 CPU 对高速缓存中的数据进行操作后,通知其他 CPU 放弃已经复制的数据在从内存中重新读取数据
对于由 volatile 申明的 Java 变量进行读写操作是 JVM 会将 CPU 发送一条带 lock 前缀的指令,将这个变量所在缓存行的数据写回系统主存中。但是就算写回系统主存,如果 CPU 高速缓存的值还是旧的在执行操作也会出现问题。为了保证各个 CPU 的高速缓存的一致性,就需要高速缓存实现缓存一致性协议:每个 CPU 就会在总线传播的数据检查自己的高速缓存中的值是否过期,当 CPU 发现自己缓存行对应数据缓存行进行地址被修改时,就会将其设置缓存行数据为无效状态,当其他 CPU 进行操作的时候就会将新的数据读取到 CPU 的高数缓存中。其中写入的模式主要是两种:
- write-Through(直写)模式:在数据进行更新的时候,同时写入低一级的高速缓存和主存中。
- 优点:操作简单,其他 CPU 获得的都是最新值,因为所有数据都会更新到主存中
- 缺点:数据写入慢,因为数据被修改后需要同时写入到低一级的高速缓存和主存中
- Write-Back(回写)模式:数据更新不会立即反应到主存中,而只是写入高速缓存中,只会将数据替换出高速缓存或者变成共享(S)状态,发现数据有变动才会将最新的数据更新到主存
- 优点:写入速度快,不会在数据变动时不需要写入到主存
- 缺点:实现比较负载,最新值可能放入在私有高速缓存中,而不是存在共享的高速缓存和主存中
MSI 协议
多核 CPU 都专有高速缓存(一般为 L1、L2),以及同一个 CPU 芯片上不同 CPU 内核之间共享高速缓存(L3),不同 CPU 内核难免加载相同的数据。这就需要用到缓存的一致性协议。
基础版本 MSI 协议,叫写入时失效协议。如果同时多个 CPU 要写入,总线会进行串行化,同一时间只会有一个 CPU 内核获得总线的访问权。
| CPU 操作 | 总线操作 | C1 缓存 | C2 缓存 | 主存 m 所在的地址 |
|---|---|---|---|---|
| 0 | ||||
| C1 读取 m | 高速缓存中没有 m,从主存中读取 | 0 | 0 | |
| C2 读取 m | 高速缓存中没有 m,从主存中读取 | 0 | 0 | 0 |
| C1 写入 1 到 m | 通知 C2,使它的高速缓存中的 m 值失效 | 1 | 0 | |
| C2 读取 m 的值 | 高速缓存中没有 m,从 C1 的高速缓存中读取(采用回写模式,并且更新到主存中) | 1 | 1 | 1 |
C2 第二次读取 m 时,C1 会将 m 的最新的值返回给 C2 并且更新主存中 m 的值,C1 和 C2 的 m 值就会变为共享状态
MESI 协议和 RFO 请求
数据行的状态
MESI 使目前主流的写入时失效协议,在 MESI 中每一个缓存行都有 4 种状态 M(Modified)、E(Exclusive)、S(Shared)、I(Invalid)
-
M:被修改(Modified)
该缓存行的数据只在本 CPU 的私有高速缓存种进行缓存,而其他 CPU 种没有,时被修改过的(Dirty),即该缓存行的数据和主存中的数据不一致而且没有更新到主存中,其该数据会在某一个时间点(其他 CPU 读取主存中相应的数据之前)写回(Write Back)主存,写回后就会变为独享状态,处于 Modified 状态的缓存行只在本 CPU 中有缓存,而且数据和内存中的数据不一致
-
E:独享的(Exclusive)
该缓存行的数据只在 CPU 的私有缓存中存在,而其他 CPU 中没有,缓存行的数据是未被修改过的(clean),并且与主存中的数据一致。当缓存行在任何时刻被其他的 CPU 读取后该缓存行就会将其变为共享状态。在 CPU 修改缓存行数据后该缓存行的状态就会变为 Modified 状态,处于 Exclusive 状态的缓存行数据只在 CPU 中有缓存,而且数据与内存中一致没有被修改过
-
S:共享的(Shared)
该缓存行的数据可能在本 CPU 以及其他 CPU 的私有高速缓存中进行缓存,并且各 CPU 私有高速缓存中的数据和主存的数据一致,当一个 CPU 修改该缓存时,其他 CPU 中该缓存行数据将作废,变为无效状态,处于 Shared 状态的缓存行的数据在多个 CPU 中都有缓存,并且与主存一致
-
I:无效的(Invalid)
该缓存行是无效的,可能有其他 CPU 修改了该缓存行
缓存行的关系
| M | E | S | I | |
|---|---|---|---|---|
| M | X | X | X | √ |
| E | X | X | X | √ |
| S | X | X | √ | √ |
| I | √ | √ | √ | √ |
状态的转换
-
初始阶段
开始的时候没有加载任何数据,处于无效状态(Invalid)
-
本地写(Local Write)阶段
如果 CPU 内核写数据处于无效状态(Invalid)的缓存行,缓存行就会将其修改为被修改状态(Modified)
-
本地读(Local Read)阶段
如果 CPU 读取处于**无效状态(Invalid)**的缓存行,此缓存没有数据给它分为两种情况:
- 其他 CPU 的缓存中也没有该数据,就会从主存加载数据到缓存行中,并变为独享状态(Exclusive),表示只有该 CPU 存在此数据
- 其他 CPU 的高速缓存中存在该数据,将此缓存行的状态设为共享状态(Shared)(处于**被修改状态(Modified)**的缓存行,再由本地 CPU 写入/读出,状态不会改变)
-
远程读(Remote Read)阶段
两个 CPU c1 和 c2 如果 C2 需要读取 C1 的缓存行的内容,C1 需要把自己的缓存行内容通过主存控制器(Memory Controller)发给 C2 , C2 接受到将对应的缓存行设置为共享状态(Shared)。在设置前主存从总线上得到这份数据并保存
-
远程写(Remote Write)阶段
C2 在得到 C1 的数据后不是为了读而是为了写,也是本地写,只是 C1 也拥有这数据的拷贝。C2 就发出一个 RFO(Request For Owner)请求,说明需要拥有这行数据的权限,其他 CPU 的相应缓存就会设置为 无效状态(Invalid),除 C2 外其他 CPU 都不能动这行数据,保证了数据的安全,但 RFO 请求以及设置为 **无效状态(Invalid)**的过程将给写操作带来很大的性能效果
| 当前状态 | 事件 | 行为 | 下一个状态 |
|---|---|---|---|
| Invalid 无效状态 | Local Read | 如果其他高速缓存中没有这份数据,本地高速缓存就从主存中读取数据 Cache Line 就会变为 E(Exclusive)共享状态 | E/S |
| 如果其他高速缓存存在这份数据,且状态为 M(Modified)被修改状态,那么将数据更新到内存,本高速缓存再从主存中读取数据,两个高速缓存的 Cache Line 状态就会变为共享的(Shared) | |||
| 如果高速缓存有这份数据,且状态为共享状态(Shared)或取独享状态(Exclusive),本高速缓存就从主存中读取数据,这些高速缓存的 Cache Line 状态都会变为共享状态(Shared) | |||
| Local Write | 从主存中读取数据,再高速缓存中修改,状态就会变为 M,如果其他高速缓存中有该份数据,其状态为 M 就会将数据更新到主存 | M | |
| 如果其他高速缓存存在这份数据,那么其他高速缓存就会将 Cache Line 状态变为 I | |||
| Remote Read | 既然是 I 状态,别的内核操作就和当前 CPU 无关 | I | |
| Remote Write | 既然是 I 状态,别的内存操作就和当前 CPU 无关 | I | |
| 独享的Exclusive | Local Read | 从高速缓存中读取数据,状态不变 | E |
| Local Write | 修改高速缓存中的数据,状态变为 M | M | |
| Remote Read | 数据和其他内核共用,状态变为 S | S | |
| Remote Write | 数据被修改,本 Cache Line 不能再使用,状态变为 I | I | |
| 共享的Shared | Local Read | 从高速缓存中读取数据,状态不变 | S |
| Local Write | 修改高速缓存中的数据,状态变为 M,其他内核共享的 Cache Line 变为 I | M | |
| Remote Read | 状态不变 | S | |
| Remote Write | 数据未被修改,本 Cache Line 不能被使用,状态变为 I | I | |
| 被修改Modified | Local Read | 从高速缓存中读取数据,状态不变 | M |
| Local Write | 修改高速缓存中的数据,状态不变 | M | |
| Remote Read | 这行数据被写入到主存中,使其他内核能使用最新的数据,状态变为 S | S | |
| Remote Write | 这行数据被写入到主存中,使其他内核能够使用最新的数据,由于其他内核会修改这行数据,状态就会变为 I | I |
Java 的 volatile 原理
java 中的 volatile 主要是保证共享变量的主存的可见性,就是将共享变量的改动值立即写入到内存。只有当共享变量使用 volatile 修饰操作系统才会检查起缓存一致性
在操作 volatile 修饰的共享变量的时候会在其汇编指令前加上 lock 前缀。lock 的功能
-
当前 CPU 缓存行的数据会立即写回到系统内存
lock 会导致调用 CPU 独占共享内存,会阻止两个 CPU 同时修改共享的内存数据
-
lock 前缀指令会引起其他 CPU 中缓存了该内存地址的数据无效
每个 CPU 会通过嗅探在总线上传播的数据来检查自己的缓存值是否过期,当发现自己的缓存行内存地址被修改后就会将当前 CPU 的缓存行设置为无状态,如果需要使用值时,就会把主存中的数据重新读取到 CPU 中
-
lock 前缀会禁止指令重排
作为内存屏障使用,禁止指令重新排序,避免出现乱序执行的现象
有序性和内存屏障
由于为了程序的运行效率,在编译的时候就会将一些不相干的指令进行重新排序,这样就会导致代码的执行的顺序和代码的编写顺序不一样,就会导致代码出现有序性问题
内存屏障(Memory Barrier)又称为内存栅栏(Memory Fences)是一系列的 CPU 指令,主要是保证特定操作的代码的执行顺序和编写顺序相同。会禁止在内存屏障前后的指令重排序
重排序
为了提高程序的性能,编译器和 CPU 会对指令进行重排序主要分为:编译器重排序和 CPU 重排序
编译器重排序
主要是程序在今年刚刚编译器编在编译阶段进行重排序,在不改变程序执行结果的情况下,编译器会对指令进行乱序(Out-of-Order)编译
主要目的是与其等待阻塞指令(如:等待缓存刷入)完成,不如去执行其他指令,与 CPU 乱序执行相比,编译器重排序能够完成更大范围,效果更好的乱序优化
CPU 重排序
流水线(Pipeline)和乱序执行(Out-of-Order Exceution)是现代 CPU 基本具有的特性。机器指令在流水线中经历指令,译码,执行,访存,写回等操作。为了提高 CPU 的执行效率,流水线是并行处理的在不影响语义的情况下,处理次序(Process Ordering CPU 实际执行顺序)和程序次序(Program Ordering,程序代码的逻辑执行顺序)是允许不一致的,只要满足 As-if-Serial 规则。
分为两类
-
指令级重排序
在不影响执行结果的情况下,CPU 内核采用 ILP(Instruction-Level-Parallelism,指令级并行运算)技术将多条指令重叠执行,主要是为了提升效率。在指令之间不存在数据依赖,CPU 就可以改变语句的对应机器指令的执行顺序这就叫指令级重排序
-
内存系统重排序
因为在 CPU 内核和主存之间存在高速缓存,在 CPU 内核进行操作时,如果缓存没有数据的话就从主存取,而对写操作都是先写入到缓存中,最后才一次性写入到内存中,主要是减少跟主存交互是 CPU 内核的短暂卡顿,提升性能,但是可能会导致缓存中的数据和主存的数据出现不一致
As-if-Serial 规则
无论如何重排序,都必须保证代码在单线程情况下运行正确。这就是 As-if-Serial
为了遵守 As-if-Serial 规则,编译器和 CPU 不会存在数据依赖关系的操作进行重排序,如果在有数据依赖的情况下进行重排序就可能改变执行的结果。但是如果指令之间不存在依赖关系也可能被编译器和 CPU 进行重排序
虽然编译器和 CPU 遵守了 As-if-Serial 规则,但也只能在单 CPU 执行情况下保证结果正确,在多核 CPU 并发的场景下,无法清晰分辨其他核上的指令是否存在依赖关系,就可能出现乱序执行,从而导致程序运行结果的错误
As-if-Serial:只能保证重排序执行后结果正确,不能保证多核以及跨 CPU 指令重排序的结果正确
硬件层面的内存屏障
在多核的情况下,所有的 CPU 操作都会遵守缓存一致性协议(MESI)校验。虽然保证了内存的可见性,但只保证内存弱可见(高速缓存失效),没有保证共享变量的强可见,而且缓存一致性协议不能禁止 CPU 的重排序,不能确保跨 CPU 的有序性
3 种内存屏障
-
读屏障
在指令前插入读屏障,可以让高速缓存中的数据失效,强制重新从主存中加载数据。并且会告诉 CPU 和编译器,先与屏障的指令必须先执行
读屏障会将当前 CPU 的共享变量的更改对所有 CPU 的内核可见,阻止了一些可能导致读取无效数据的指令重排
-
写屏障
在指令后插入写屏障,能让高速缓存中最新的数据更新到主存,让其他线程可见,并且写屏障会告诉 CPU 和编译器后于这个屏障的指令必须执行
-
全屏障
是一种全能的屏障,具备读屏障和写屏障的能力,Full Barrier 称为 StoreLoad Barriers 对应 X86 处理器的 nfence 指令
X86 处理禁止对 mfence 指令前后的 store/load 指令进行重排序
内存屏障的作用
-
阻止屏障两侧的指令重排序
插入内存屏障后,先于这个屏障的指令必须执行,后于这个屏障的指令必须执行
-
请求让高速缓存的数据失效
会让处理的数据强制写入到内存中,让高速缓存中的数据失效,让其他 CPU 获得最新的值
private volatile int x = 0; // 对 x 进行修饰
private Boolean flag = false;
public void update(){
x = 8;
// volatile 编译器会插入 store Barrier 写屏障
flag = true;
}
public void show(){
if (flag){
System.out.println(x);
}
}
JMM(java 内存模型)
JMM 是 java 内存模型,JMM 是一种 Java 对于内存运算的一些规范和规则
- 主存:主要存在 Java 的实例对象,所有创建并实例化的对象都存储在主存中,不论是实例的成员变量还是方法中的局部变量,也包括共享的类信息,常量,静态变量。主存是共享的所有多线程情况下访问的时候就会出现线程安全问题
- 工作内存:主要存储当前方法的本地变量(主要是获取主存中的数据,是主存中的对应的数据的副本),工作内存是每一个线程独立的,对其他线程是不可见的,所以工作内存不存在线程的安全问题
Java 内存的规定
- 所有变量存储在主存中
- 每个线程都有自己的工作内存,且对变量的操作都是在工作内存中进行的
- 不同线程之间无法直接访问彼此的变量,只能通过主存传递
JMM 的 操作
这是 JMM 定义的一套自己的主存与工作内存之间的交互协议。就是一个共享变量是如何从主存和工作内存之间的操作
| 操作 | 作用对象 | 说明 |
|---|---|---|
| Read(读取) | 主存 | 将一个变量值从主存传输到工作内存中,以便随后的 Load 操作 |
| Load(载入) | 工作内存 | Load 操作把 Read 操作重主存中得到的变量载入到工作内存的变量副本中。变量副本可以理解为 CPU 的高速缓存 |
| Use(使用) | 工作内存 | Use 操作就是把 Read 操作重主存中得到的变量值叫给执行引擎。每当 JVM 遇到一个需要使用变量值的字节码时,执行 Use 操作 |
| Assign(赋值) | 工作内存 | 执行引擎通过 Assign 操作给工作内存变量赋值。每当 JVM 遇到变量赋值的字节码指令时,执行 Assign 操作 |
| Store(存储) | 工作内存 | Store 就是把工作内存中的一个变量传递到主存中,以便 write 操作使用 |
| Write(写入) | 主存 | Write 操作把 Store 操作从工作内存中得到的变量值放入到主存的变量中 |
| Lock(锁定) | 主存 | 把一个变量标识为线程独享状态 |
| Unlock(解锁) | 主存 | 将一个处于 Lock 状态的变量释放,以便其他线程进行操作 |
JMM 要求 Read 和 Load,Store 和 Write 必须按顺序执行,但不要求连续执行,Read 和 Load 之间,Store 和 Write 之间可以插入其他的操作
JMM 还必须满足规则
- 不允许 Read 和 load,Store 和 Write 操作之一单独出现,以上的操作必须按顺序执行。不要求连续执行,在之间可以插入其他的操作。Read 就有 Load 不能读取了变量值而不予加载工作内存中,Store 和 Write,也不能存储了变量而不写入到主存中
- 不允许一个线程丢弃最近的 Assign 操作。线程使用 Assagin 操作对私有内存变量进行变更是,必须使用 Write 操作写入到主存中
- 不允许一个线程无原因的(没有发生过任何的 Assign 操作)把数据从工作内存同步到主存中
- 一个新的变量只能从主存中诞生,不允许在工作内存中直接使用一个未被初始化(Load 和 Assign)的变量,对一个变量实施 Use 和 Store 操作之前必须先执行 Assign 和 Load 操作
- 一个变量在同一个时刻只允许一个线程对其执行 Lock 操作,如果执行多次 Lock 操作也必须执行相同次数的 unLock 操作
- 如果对一个变量执行 Lock 操作,将清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行 Load 或 Assign 操作初始化变量值
- 如果变量没有被 Lock 操作锁定,就不允许执行 unLock 操作,也不允许 unLock 操作未被锁定的变量
- 对变量执行 unLock 操作之前必须将其同步到主存中(执行 store 和 write 操作)
JMM 解决有序性问题
由于不同的 CPU 硬件实现内存屏障的方式不同,JMM 就屏蔽了底层对 CPU 硬件平台的差异,定义了 JMM 对硬件层的逻辑。
JMM 内存屏障主要有 Load 和 Store 两类
- Load Barrier(读屏障):在读指令前插入读屏障,可以让高速缓存中的数据失效,强制加载主存中的数据
- Store Barrier(写屏障):在写指令之后插入写屏障,能将数据强制写入到主存中
实际运用中使用 Load 和 Store 进行组合而成,用于禁止特定类型 CPU 禁止重排序
-
LoadLoad(LL)屏障
在执行预加载(支持乱序处理)的指令序列中,通常显示的声明 LL 屏障,执行可能会依赖其他 CPU 执行的 Load 的结果
实例:Load1;LoadLoad;Load2
在 Load2 读取的数据被访问前,使用 LoadLoad 屏障保证 Load1 要读取的数据被读取完成
-
StoreStore(SS)屏障
如果 CPU 不能保证从高速缓存向主存按顺序刷新数据,就会使用 StoreStore 屏障
实例:Store1;StoreStore;Store2
在 Store2 及后续的写入操作前,SS 屏障必须保证 Store1 对其他 CPU 可见
-
LoadStore(LS)屏障
用于在写入操作前确保数据完成读取实例:Load1;LoadStore;Store2
在 Store2 及后续的写入操作前,LS 屏障保证 Load1 读取的数据读取完成
-
StoreLoad(SL)屏障
用于读取数据前,保证数据完全写入
实例:Store1;StoreLoad;Load2
在 Load2 读取前,SL 屏障保证 Store1 的写入对所有的 CPU 可见
SL 屏障的开销最大,但是是一个全能型的屏障,具有其他 3 种屏障的效果。
Volatile 关键字的内存屏障
volatile 关键字的语义
- 不同线程对 volatile 修饰的值,队内存具有可见性(一个线程修改了此值会立即对其他线程立即可见)
- 禁止指令重排序
volatile 插入屏障的策略(保守策略)
- 在每个 volatile 读操作前面插入一个 LoadLoad 屏障
- 在每个 volatile 读操作后面插入一个 LoadStore 屏障
- 在每个 volatile 写操作前面插入一个 StoreStore 屏障
- 在每个 volatile 写操作后面插入一个 StoreLoad 屏障
主要策略是:在每个 volatile 写操作前插入 StoreStore 屏障,在写操作后插入 StoreLoar 屏障
volatile 写操作的插入策略:在 volatile 写前插入 StoreStIIIIIIIIIIIIore 屏障(禁止前面的普通读写操作和下面的 volatile 写重排序)在 volatile 写后插入 StoreLoad 屏障(禁止上面的 volatile 写和下面可能的 volatile 读/写重排序)
volatile 读操作的策略:在 volatile 读操作前插入 LoadLoad 屏障(禁止下面的普通读和上面的 volatile 读进行重新排序)在 volatile 读后插入 LoadStore 屏障(禁止下面的普通写和上面的 volatile 读禁止重排序)
volatile 不具备原子性
volatile 使用的重要语义
- 使用 volatile 修饰的变量在发送变化的时候会立即同步到内存,并使其他线程中的副本失效
- 禁止指令重排序:通过内存屏障实现的
JMM 对 volatile 特殊的约束
- volatile 修饰的变量其 read,load,use 都是连续出现的,所以每次使用变量的时候都要从主存中获取最新的值,替换内存中的变量副本
- 对同一变量的 assign,stire,write 操作都是连续出现的,所以每次对变量的改变都会马上同步到主存中
不保证原子性的原因
虽然 volatile 变量可以强制刷新到内存中,但是不具备原子性(操作中可以插入其他的操作)。虽然对变量的(read,load,use),(assign,store,write)必须连续出现,但是在不同的 CPU 执行时可能读取到脏数据
volatile 变量不保证复合操作具有原子性,如果需要原子性就需要使用锁
Hppens-Before 规则
程序顺序执行规则(as-if-serial)
在同一个线程中,有依赖关系的操作必须按照书写的先后顺序执行(无论如何进行重排序,执行的结果都不会发生改变)
一个线程内,按照代码顺序,书写在前面的操作先行发生(Happens-Before)于书写在后面的操作
volatile 变量规则
对 volatile 修饰的变量的写操作必须先发生对 volatile 的读操作
volatile 指令之间是否可以重排序
| 普通读写 | volatile 读 | volatile 写 | |
|---|---|---|---|
| 普通读写 | √ | √ | X |
| volatile 读 | X | X | X |
| volatile 写 | √ | X | X |
如果第二个操作为 volatile 写,无论第一个操作是什么都不会进行重排序,确保 volatile 写之前的操作不会被排序到自己后
如果第一个操作为 volatile 读,无论第二个操作是什么都不能从排序,确保 volatile 读之后的操作不能被重排序到自己前面
传递性规则
如果 A 操作先于 B 操作发生,而且 C 操作先于 B 操作后发生,那么 A 操作必须先于 C 操作发生
监视锁规则(Monitor Lock Rule)
对一个锁的 unlock 操作先行于后面对同一个锁的 lock 操作,不论在多线程还是单线程环境中,同一个锁处于锁定状态,那么必须先对锁进行释放操作,后面才会进行 lock 操作
在加锁和解锁之间的指令可以进行重排序
start 规则
如果 A 线程执行了 start() 操作启动线程 B,那么 A 线程之中的操作先行发生于线程 B 中的任何操作
public static void main(String[] args) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " ...... start");
System.out.println(" ......");
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " ...... start");
System.out.println(" ......");
System.out.println(Thread.currentThread().getName() + " ...... end");
},"B").start();
System.out.println(Thread.currentThread().getName() + " ...... end");
},"A").start();
}
join 规则
如果线程 A 执行 join() 操作并成功返回,那么 B 线程中的任意操作先行发送于线程 A 的 join() 操作
2217

被折叠的 条评论
为什么被折叠?



