一、认识 volatile 关键字
当一个线程读取,一个线程写入的情况下,就会存在数据的不一致性,
二、CPU 与 JMM
1、CPU Cache 模型
每一个 CPU 在修改内存数据的步骤如下:
- 从内存中把数据加载到 Cache 中
- 从 Cache 中读取数据加载到寄存器中
- 在寄存器中更新数据
- 把更新结果写入 Cache
- 把更新结果写入到内存中
解决办法
- 总线(BUS)加锁:封装的粒度太大,效率比较低
- MESI 协议:
- 读操作,不做任何事情,把 Cache 中的数据读到寄存器
- 写操作,发出信号,通知其他的 CPU 将变量的 Cache line 置为无效状态,其他的 CPU 要访问这个变量的时候,只能从内存中获取
2、JMM 内存模型

- 主内存中的数据是所有线程都可以访问的
- 每个线程都有自己的工作空间
- 工作空间中的数据:局部变量、主内存的副本
- 线程不能直接修改内存中的数据,只能读到工作空间来进行修改,修改完成后,刷新到主内存
三、volatile 语义分析
当变量使用 volatile 关键字修饰时,当某一个线程修改这个变量时,会发出 JMM 控制信号(相当于设置了 Cache line 无效),那么其他线程要读取整个变量的时候,就只能从主内存中读取,那么问题来了,volitail 也不能保证数据的一致性(原子性)。
1、保证可见性
- volatile 作用:让其他线程能够马上感知到某一线程对某个变量的修改
- 不能保证原子性
2、保证有序性
在编译和指令优化阶段,在输入的代码顺序并不一定是程序实际执行的顺序,这就叫做指令重排(为了提高 CPU的吞吐量)。指令重排必须保证结果不发生变化
- 单线程的情况下,指令重排后没有影响(as-if-serial)
- 多线程的情况下,可能造成影响
当使用 volatile 关键字的时候,可以保证指令按照程序的顺序执行,volatile 的规则如下:
- volatile 之前的代码不允许调整到它的后面
- volatile 之后的代码不允许调整到它的前面
四、volatile 实现原理和机制
volatile 的实现其实就是锁的原理(轻量级锁)
volatile int a; ==> LOCK: a
五、volatile 与 synchronized 的区别
- 使用上的区别:volatile 只能修饰变量,synchronized 可以修饰方法和代码块
- 对原子性的保证:volatile 不能保证原子性,synchronized 可以保证原子性
- 对可见性的保证:都可以保证可见性,但实现原理不同,volatile 使用 LOCK 来实现,synchronized 使用的是 monitor 来实现(monitorenter 和 monitorexit)
- 对有序性的保证:volatile 能保证有序性,synchronized 也可以保证有序性,但是代价太大(重量级锁会退化到串行)
- 其他:volatile **不会引起线程阻塞,synchronized 会引起线程阻塞**
六、volatile 的使用场景
- 状态(开关)模式
- 双重检查锁定 DCL(Double-Checked-Locking):如单例模式的应用 volatile + synchronized
- 需要利用顺序性
七、volatile 重排序规则
| 是否能重排序 | 第二个操作 | ||
|---|---|---|---|
| 第一个操作 | 普通读/写 | volatile读 | volatile写 |
| 普通读/写 | NO | ||
| volatile 读 | NO | NO | NO |
| volatile 写 | NO | NO |
结论:
1、第二个操作是 volatile 写,不管第一个操作是什么,都不会重排序
2、第一个操作是 volatile 读,不管第二个操作是什么,都不会重排序
3、第一个操作是 volatile 写,第二个操作时 volatile 的话,也不会发生重排序
X86 处理器不会对 读-读,读-写,写-写 操作做重排序,会省略掉这三类操作的内存屏障,仅会对写-读做重排序,所以 volatile 写-读操作只需要在 volatile 后面加一个 StoreLoad 屏障
JVM 内存屏障插入策略
1、在每个 volatile 写操作的前面插入一个 StroeStroe 屏障
2、在每个 volatile 写操作的后面插入一个 StroeLoad 屏障
3、在每个 volatile 读操作的后面插入一个 LoadLoad 屏障
4、在每个 volatile 读操作的后面插入一个 LoadStore 屏障

