对 volatile 的理解

本文探讨了Java中的volatile关键字,它确保变量的修改对所有线程可见,通过内存屏障防止指令重排序。文章还介绍了Java内存模型,包括主内存和工作内存,以及并发过程的原子性、可见性和有序性。同时,详细阐述了MESI缓存一致性协议,并讨论了volatile在实现这些特性中的作用和应用场景。
摘要由CSDN通过智能技术生成

NOTICE:本文仅记录本人对 volatile 关键字的小小理解,没有详细记录每个点,若有误可指出

一个对象的产生

java 的 Class 对象产生会经历以下阶段:类加载,验证,准备,解析,初始化

  1. 类加载:通过类的全限定名获取类的二进制,并转换成 JVM 的方法区的 Class 对象
  2. 验证:对 Class 对象进行格式上的验证,分别有文件格式验证,元数据验证,字节码验证,符号引用验证
  3. 准备:给 Class 对象的 static 变量分配内存并赋初始零值
  4. 解析:姜符号引用转换成直接引用
  5. 初始化:执行 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)的协议

状态机
  1. Modify
  2. Exclusive
  3. Shared
  4. 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 拉取主存最新数据
缓存 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 关键字

主要特性
  1. volatile 修饰的变量的修改对于所有线程具有可见性
    1. volatile 通过在操作变量前后插入内存屏障
    2. volatile 修饰的变量在 assign 并 write 回到主内存后,通知其他线程值被改变,详细步骤参考 MESI 协议的状态流转
  2. volatile 禁止机器指令重排序
  3. volatile 不保证原子性
    1. 若前一线程读取变量后被阻塞,后一线程修改后写回主存,前一线程后续修改后写回主存,就出现主存的数据不一致的现象
适用场景
  1. 一次性重要事件,比如程序关闭赋值某个 boolean 变量
  2. double check lock,防止指令重排序导致访问到未初始化对象
底层实现

volatile 通过内存屏障实现可见性

写操作

写操作前插入 StoreStore 屏障,确保修改对其他线程可见

写操作后插入 StoreLoad 屏障,确保其他线程在读取数据时能读取最新的数据

读操作

读操作前,插入 LoadLoad 屏障,确保所有线程拿到的数据都是一样的

读操作后,插入 LoadStore 屏障,确保当前线程在其他线程修改前获取最新的值

内存屏障

屏障执行顺序解释
LoadLoadLoad1 -> LoadLoad -> Load2Load2 读取数据前,保证 Load1 读取数据读取完毕
StoreStoreStore1 -> StoreStore -> Store2Store2 写入执行前,保证 Store1 写入对其他处理器可见
LoadStoreLoad1 -> LoadStore -> Store2Store2 写入执行前,保证 Load1 读取数据读取完毕
StoreLoadStore1 -> StoreLoad -> Load2Load2 读取前,保证 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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值