对 volatile 的理解

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 屏障,确保当前线程在其他线程修改前获取最新的值

内存屏障

屏障 执行顺序 解释
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

你可能感兴趣的:(java,jvm,java,面试)