volatile
解释
volatile修饰的变量能够保证可见性,但不保证原子性,每个线程能够获取该变量的最新值。
如何保持可见性
对volatile修饰的词,程序在编译的时候,会多一个lock汇编指令如下图所示:
该lock指令有两个主要作用:
- 将当前缓存行的数据回写到内存中
- 使其他cpu里缓存了该内存地址的数据无效(缓存一致性机制)
JMM主要是通过设置内存屏障来禁止指令重排序,下图是汇编程序中的四种内存屏障类型
屏障类型 | 说明 |
---|---|
LoadLoad Barriers | 保证Load1数据的装载先于Load2及所有后续指令的装载 |
StoreStore Barriers | 保证Store1数据对其他处理器的可见先于Store2及所有后续存储指令的存储 |
LoadStore Barriers | 确保Load1数据装载先于Store2及所有后续的存储指令刷新到内存 |
StoreLoad Barriers | 确保Store1数据对其他处理器的可见先于Load2及所有后续装载指令的装载 |
volatile是如何防止指令重排序的:
- volatile写操作前面插入一个StoreStore屏障
- volatile写操作后面插入一个StoreLoad屏障
- volatile读操作后面插入一个LoadLoad屏障
- volatile读操作后面插入一个LoadStore屏障
适用场景
在使用volatile时需满足一下两个条件:
- 对变量的写操作不依赖于当前值
- 该变量没有包含在具有其他变量的不变式中
- 不能保持原子性
volatile和synchronized
- 使用区别:volatile只能修饰变量,synchronized只能修饰方法和语句块
- 原子性:synchronized可以保证原子性,volatile不能保证原子性
- 可见性:都可以保证可见性,但实现原理不同volatile对变量加了lock,synchronized使用monitorEnter和monitorexit monitor JVM
- 有序性:volatile能保证有序,synchronized可以保证有序性,但是代价(重量级)并发退化到串行
- 线程阻塞:synchronized会引起线程阻塞,volatitle不会引起线程阻塞
CPU高速缓存
CPU缓存是位于CPU与内存之间的临时存储器,它的容量比内存小的多但是交换速度却比内存要快得多。高速缓存的出现主要是为了解决CPU运算速度与内存读写速度不匹配的矛盾,因为CPU运算速度要比内存读写速度快很多,这样会使CPU花费很长时间等待数据到来或把数据写入内存。在缓存中的数据是内存中的一小部分,但这一小部分是短时间内CPU即将访问的,当CPU调用大量数据时,就可避开内存直接从缓存中调用,从而加快读取速度。
下图为CPU高级缓存层次图:
在引入CPU告诉缓存后,会伴随着缓存不一致的情况的出现,针对此问题,在CPU层引入:
- 总线锁
- 缓存锁
缓存锁的核心是MESI(缓存一致性协议)
MESI
mesi表是缓存的四种状态,分别是:
Happen-before规则
- 程序顺序规则,程序代码顺序,书写在前面的操作先行于书写在后面的操作,控制流顺序不是代码顺序还要考虑分支和循环。
- 管程锁定规则,一个unlock操作一定发生在同一个锁的lock操作之后。
- volatitle规则,对volatitle操作一定写一定先于后面的读操作
线程启动规则,start方法先于所有操作 - 线程终止规则,所有操作都先于线程终止检查
- 线程中断规则,对线程interrupt方法调用先于线程终止检查到中断事件发生。
- 对象终结规则,一个对象的初始化完成,先行发生于它的finalize的方法开始。
- 传递性
- synchronized监控器锁