一、Volatile保证可见性
- Volatile是通过MESI缓存一致性协议来保证可见性的
- 什么是MESI协议?
- MESI协议其实是一个变量在内存中的不同状态!MESI 是指4中状态的首字
- M 修改 (Modified) 当一个线程要修改便令
- E 独享、互斥 (Exclusive) 当一个线程拿到了共享变量,此时为独享状态!
- S 共享 (Shared) 当多个线程都拿到了共享变量,此时为共享状态!
- I 无效 (Invalid) 线程丢弃了自己工作内存中的变量,为无效状态!
- MESI协议其实是一个变量在内存中的不同状态!MESI 是指4中状态的首字
- MESI协议如何保证可见性?
- 首先cpu会根据共享变量是否带有Volatile字段,来决定是否使用MESI协议保证缓存一致性。
- 如果有Volatile,汇编层面会对变量加上Lock前缀,当一个线程修改变量的值后,会马上经过store、write等原子操作修改主内存的值(如果不加Lock前缀不会马上同步),为什么监听到修改会马上同步呢?就是为了触发cpu的嗅探机制,及时失效其他线程变量副本。
- cpu总线嗅探机制
- cpu总线嗅探机制监听到这个变量被修改,就会把其他线程的变量副本由共享S置为无效I,当其他线程在使用变量副本时,发现其已经无效,就回去主内存中拿一个最新的值。
- 在写入主内存时为什么要加锁?在哪里加锁?
- 变量被修改后同步到主内存的过程中会在store之前加锁,写完后解锁,这个锁只有在修改的时候才会加,锁粒度非常小。
- 因为在store时可能已经经过了总线,但此时还没有write进主内存,总线却触发了嗅探机制,其他线程的变量已失效,当其他线程去主内存读最新数据时,新数据还未write进来,产生脏数据!
- Lock前缀的作用
- 使CPU缓存数据立即写会主内存(Volatile修饰的变量会带lock前缀)
- 触发总线嗅探机制和缓存一致性协议MESI来失效其他线程的变量
二、Volatile保证有序性
-
多线程环境下,有序性问题产生的主要原因就是执行重排优化,而Volatile的另一个作用就是禁止指令重排优化。具体是通过对Volatile修饰的变量增加内存屏障来完成的!
-
写屏障【给Volatile变量赋值】
- 写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后
public void actor2(I_Result r) { num = 2; ready = true; // ready是被volatile修饰的 , 赋值带写屏障 // 写屏障 }
-
读屏障【读取Volatile变量的值】
- 读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前
public void actor1(I_Result r) { // 读屏障 // ready是被volatile修饰的 ,读取值带读屏障 if(ready) { r.r1 = num + num; } else { r.r1 = 1; } }
-
注意:【因此Volatile无法保证原子性】
- 写屏障仅仅是保证之后的读能够读到最新的结果,但不能保证其它线程的读跑到它前面去。有序性的保证只是保证了本线程内相关代码不被重排序,无法保证其他线程的读写指令交错,所以Volatile无法保证原子性!!!!!
参考文献:
volatile是怎么保证可见性和有序性的
黑马程序员JUC课程