对 volatile 的理解
NOTICE:本文仅记录本人对 volatile 关键字的小小理解,没有详细记录每个点,若有误可指出
一个对象的产生
java 的 Class 对象产生会经历以下阶段:类加载,验证,准备,解析,初始化
- 类加载:通过类的全限定名获取类的二进制,并转换成 JVM 的方法区的 Class 对象
- 验证:对 Class 对象进行格式上的验证,分别有文件格式验证,元数据验证,字节码验证,符号引用验证
- 准备:给 Class 对象的 static 变量分配内存并赋初始零值
- 解析:姜符号引用转换成直接引用
- 初始化:执行 Class 文件显式给 static 变量赋值语句
若运行时需要使用 Class 对应的对象时,会使用 new 关键字或者 newInstance 方法创建,于是 JVM 调用 Class 的元信息,在堆,运行时常量池划分一块内存放入新建的对象
如果对象是在虚拟机栈上,使用的是局部变量,那程序一直执行下去,没问题。
但是如果使用的是成员变量,并发修改,并且想要看到是对的(可见性),那别的修改需要修改后写回到主存,并且在用的时候也要拉到最新的数据,这涉及到 JAVA 的内存模型,以及缓存一致性协议
JAVA 内存模型
JAVA 内存模型主要分为两类:主内存和工作内存
主内存是所有变量存储的地方,工作内存是线程具体工作的地方,使用的是主内存的变量副本
这里就涉及主内存与工作内存的同步问题,涉及到并发三特性以及内存间交互操作
并发过程的三特性
原子性
一个操作/多个操作要么执行成功,要不都不执行,类似于事务
可见性
一个线程对变量进行操作,其余线程能立刻看到变更,这个就解决了变更后线程间不一致的问题
有序性
程序执行顺序按照控制流顺序执行
内存间交互操作
- lock
[主内存] 将某个变量标为该线程独占的状态
- read
[主内存 -> 工作内存] 将主内存中变量的值 copy 到工作内存中
- load
[工作内存] 将 read 过程中变量的值赋给变量副本
- use
[工作内存] 代码内使用变量
- assign
[工作内存] 将代码过程中变更的值赋给工作内存变量副本
- store
[工作内存 -> 主内存] 将工作内存变量副本的值传回到主内存
- write
[主内存] 将传回来的值写回到主内存变量中
- unlock
将变量从独占状态释放
- 概括来说
一个线程使用某个变量,赋值给某个变量,需要在主内存,工作内存中互相复制,必须要经过的步骤:read -> load -> use -> assign -> store -> write
若想线程想独占这个变量,两个方式:该变量是局部变量,用 volatile 修饰该全局变量
缓存一致性协议
MESI
MESI 是一种基于回写(write-back)、缓存无效化(invalidate)的协议
状态机
- Modify
- Exclusive
- Shared
- Invalid
状态变更(缓存 A / 缓存 B / 主存)
状态变更 | 前提 | 动作 |
---|---|---|
Modify -> Modify | / | local read/local write,状态不发生变化 |
Modify -> Invalid | 缓存 A / B 同时含有数据 | 前一时间点缓存 A 已更新,缓存 B 接收 Invalid message ,将该缓存置为 Invalid |
Modify -> Shared | 缓存 A 更新了本地数据,缓存 B 无数据 | 缓存 A write back 到主存,缓存 B 拉取主存最新数据<br />缓存 A 数据从 Modify -> Shared |
状态变更 | 前提 | 动作 |
---|---|---|
Exclusive -> Exclusive | / | 缓存 A local read |
Exclusive -> Modify | 缓存 A 缓存 Exclusive | 缓存 A local write |
Exclusive -> Shared | 缓存 A 缓存 Exclusive | 缓存 B 读主存数据,数据从 Exclusive 变为 Shared |
Exclusive -> Invalid | 缓存 A 缓存 Exclusive | 缓存 A local write,将数据置为 Invalid |
状态变更 | 前提 | 动作 |
---|---|---|
Shared -> Shared | / | 缓存 A local read / 缓存 A/B 同时读同一份数据 |
Shared -> Invalid | 缓存 A 修改了缓存 | 缓存 B 接收 Invalid 事件,并将自身置为 Invalid |
Shared -> Modify | / | 缓存 A local write |
状态变更 | 前提 | 动作 |
---|---|---|
Invalid -> Invalid | 缓存 B 缓存 Invalid | 缓存 A Modify 缓存,缓存 B 缓存从 Invalid 到 Invalid |
Invalid -> Shared / Exclusive | / | 缓存 B 拉取最新的数据,若缓存 A 有数据,则为 Shared ,不然为 Exclusive |
Invalid -> Modify | / | 缓存 A 拉取最新数据,并 local write,状态为 Modify |
缓存一致性在 JVM 中落地 -- volatile 关键字
主要特性
- volatile 修饰的变量的修改对于所有线程具有可见性
- volatile 通过在操作变量前后插入内存屏障
- volatile 修饰的变量在 assign 并 write 回到主内存后,通知其他线程值被改变,详细步骤参考 MESI 协议的状态流转
- volatile 禁止机器指令重排序
- volatile 不保证原子性
- 若前一线程读取变量后被阻塞,后一线程修改后写回主存,前一线程后续修改后写回主存,就出现主存的数据不一致的现象
适用场景
- 一次性重要事件,比如程序关闭赋值某个 boolean 变量
- double check lock,防止指令重排序导致访问到未初始化对象
底层实现
volatile 通过内存屏障实现可见性
写操作
写操作前插入 StoreStore 屏障,确保修改对其他线程可见
写操作后插入 StoreLoad 屏障,确保其他线程在读取数据时能读取最新的数据
读操作
读操作前,插入 LoadLoad 屏障,确保所有线程拿到的数据都是一样的
读操作后,插入 LoadStore 屏障,确保当前线程在其他线程修改前获取最新的值
内存屏障
屏障 | 执行顺序 | 解释 |
---|---|---|
LoadLoad | Load1 -> LoadLoad -> Load2 | Load2 读取数据前,保证 Load1 读取数据读取完毕 |
StoreStore | Store1 -> StoreStore -> Store2 | Store2 写入执行前,保证 Store1 写入对其他处理器可见 |
LoadStore | Load1 -> LoadStore -> Store2 | Store2 写入执行前,保证 Load1 读取数据读取完毕 |
StoreLoad | Store1 -> StoreLoad -> Load2 | Load2 读取前,保证 Store1 写入对所有处理器可见 |
指令重排序
CPU 为了运行效率,会对指令进行重排序,后面代码可能会先于前面的代码执行
重排序也遵循 as-if-serial 语义以及 happen-before 原则
as-if-serial 语义
存在数据依赖关系的先后操作不会重排序
happen-before 原则
先行发生原则,先行发生的操作产生的影响能被后续的操作获取
分类 | 说明 |
---|---|
程序次序规则 | 控制流顺序,前面代码一定会先于后面代码执行 |
管程锁定原则 | 锁的 lock 操作先于 unlock 操作执行 |
volatile 变量原则 | volatile 变量的写操作先于读操作执行 |
线程启动原则 | 线程的 start 方法先于线程任一操作执行 |
线程终止原则 | 线程任一操作先于对线程的终止操作 |
线程中断原则 | 线程的 interrupt 方法调用先于线程任一检测中断事件操作 |
对象终结原则 | 对象的初始化完成先于 finalize 方法的调用 |
传递性 | A -> B,B -> C,那么 A -> C |