共计 2921 个字符,预计需要花费 8 分钟才能阅读完成。
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 拉取主存最新数据 缓存 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 |