NOTICE:本文仅记录本人对 volatile 关键字的小小理解,没有详细记录每个点,若有误可指出
java 的 Class 对象产生会经历以下阶段:类加载,验证,准备,解析,初始化
若运行时需要使用 Class 对应的对象时,会使用 new 关键字或者 newInstance 方法创建,于是 JVM 调用 Class 的元信息,在堆,运行时常量池划分一块内存放入新建的对象
如果对象是在虚拟机栈上,使用的是局部变量,那程序一直执行下去,没问题。
但是如果使用的是成员变量,并发修改,并且想要看到是对的(可见性),那别的修改需要修改后写回到主存,并且在用的时候也要拉到最新的数据,这涉及到 JAVA 的内存模型,以及缓存一致性协议
JAVA 内存模型主要分为两类:主内存和工作内存
主内存是所有变量存储的地方,工作内存是线程具体工作的地方,使用的是主内存的变量副本
这里就涉及主内存与工作内存的同步问题,涉及到并发三特性以及内存间交互操作
一个操作/多个操作要么执行成功,要不都不执行,类似于事务
一个线程对变量进行操作,其余线程能立刻看到变更,这个就解决了变更后线程间不一致的问题
程序执行顺序按照控制流顺序执行
[主内存] 将某个变量标为该线程独占的状态
[主内存 -> 工作内存] 将主内存中变量的值 copy 到工作内存中
[工作内存] 将 read 过程中变量的值赋给变量副本
[工作内存] 代码内使用变量
[工作内存] 将代码过程中变更的值赋给工作内存变量副本
[工作内存 -> 主内存] 将工作内存变量副本的值传回到主内存
[主内存] 将传回来的值写回到主内存变量中
将变量从独占状态释放
一个线程使用某个变量,赋值给某个变量,需要在主内存,工作内存中互相复制,必须要经过的步骤:read -> load -> use -> assign -> store -> write
若想线程想独占这个变量,两个方式:该变量是局部变量,用 volatile 修饰该全局变量
MESI 是一种基于回写(write-back)、缓存无效化(invalidate)的协议
状态变更 | 前提 | 动作 |
---|---|---|
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 |
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 原则
存在数据依赖关系的先后操作不会重排序
先行发生原则,先行发生的操作产生的影响能被后续的操作获取
分类 | 说明 |
---|---|
程序次序规则 | 控制流顺序,前面代码一定会先于后面代码执行 |
管程锁定原则 | 锁的 lock 操作先于 unlock 操作执行 |
volatile 变量原则 | volatile 变量的写操作先于读操作执行 |
线程启动原则 | 线程的 start 方法先于线程任一操作执行 |
线程终止原则 | 线程任一操作先于对线程的终止操作 |
线程中断原则 | 线程的 interrupt 方法调用先于线程任一检测中断事件操作 |
对象终结原则 | 对象的初始化完成先于 finalize 方法的调用 |
传递性 | A -> B,B -> C,那么 A -> C |