目录
1. 前言
不知道从什么时候开始,百度volatile相关的文章基本上都是一个套路:
先从CPU缓存架构、计算机的内存模型、MESI、volatile实时触发数据一致性……
或许是从某个培训机构讲师嘴里说出来的,也可能是某位P9大佬呕心沥血花了几个月整理出来最后被共同封杀的笔记里记载的。
一开始我也对这套理论坚信不疑,但最近看了一段相关的资料,发现这套理论一些细节并不能站稳脚跟,或者说讲得不够透彻。(PS:默默把自己之前发布的几篇相关文章删了)
首先强调一点,volatile和MESI这两个东西没有必然关系。MESI是缓存一致性的一种实现手段,而volatile是在java层面只是jvm这款软件的一段代码增强,意图是保证变量的可见性和有序性。
2. volatile的作用与原理
volatile在java层面只是JVM这款软件的一段代码增强,意图是保证变量的可见性和有序性。
首先从可见性来说,虽然有缓存一致性协议可以保证各个CPU从缓存到主存之间的一致性。但问题是,数据得先到高速缓存才行啊,它可能还在写缓冲区storeBuffer。而且,对于有的CPU架构,还有无效化队列invalidQueue。
而volatile的目的就是告诉cpu,这个变量不需要缓冲区中而是每次都强制刷到缓存。只要刷到缓存,因为MESI或者其它缓存一致性协议的实现,各CPU缓存一致,所以即可实现可见性
而有序性则是做了两件事情:
- 禁止编译器进行指令重排序
- 使用内存屏障来避免storeBuffer和invaildQueue造成的指令乱序
3. 为什么有了MESI还需要volatile关键字
其实理解了上面你就可以不用读这个了。
明确一个点,缓存一致性协议是为了保证多个cache与内存之间的数据同步,但是volatile的语义是为了从软件层面来保证可见性和有序性。
它并不关注底层是通过什么技术来实现的,因此单靠MESI并不能满足volatile,它只是volatile语义实现的一部分
MESI也是默认生效的,要不然你以为操作系统线程间的数据可见性是怎么保证的,别再说什么volatile触发了MESI……
4. 内存屏障是什么?它是怎么保证有序性的?
内存屏障是干嘛的?防止指令重排序嘛,为什么会有指令重排序?指令重排是什么?怎么防止的?
其实CPU真没有智能到能自己重排硬件指令,所有重排几乎都是storeBuffer和invaildQueue造成的,大部分CPU只是按照简单流水线方式去执行硬件指令
不过编译器的指令重排确实是由volatile禁止了。
4.1 Store Buffer & Invalid Queue
MESI机制不止四个状态那么简单,除了四个状态之外,还有一套消息机制,正是因为这套消息机制带来了性能的下降, 于是也就有了 storeBuffer, invalidQueue等优化措施。
但是这些优化措施又带来了另外的问题, 一些立马需要被其他CPU核心感知的修改, 因为storeBuffer的优化迟迟无法写入cache,MESI机制无法生效, 其他CPU核心就感知不到。
所以CPU就提供了读、写屏障指令,让程序员或编译器明确声明,这里的修改需要立即写入cache, 不能在storeBuffer里存着,效果就是修改完变量后, 需要立即刷storeBuffer里的数据到cache,不能等CPU空闲时再刷。
InvalidQueue差不多亦是如此。
读、写屏障指令的本质就是把默认的异步刷buffer, 强制切换成同步刷buffer。
5. synchronized是怎么保证可见性的?
JMM中关于synchronized有如下规定,线程加锁时,必须清空工作内存中共享变量的值,从而使用共享变量时需要从主内存重新读取;线程在解锁时,需要把工作内存中最新的共享变量的值写入到主存,以此来保证共享变量的可见性。
有序性?拜托,代码块都只允许一个线程访问了,它乱不乱序有什么影响吗?
6. 相关资料
这里留一些相关资料,本文大部分也是参考其中。
我并没有去验证过这些理论的真实性,但至少这套理论是能够逻辑自洽的,并且深度超越了目前网络上大部分博文,看完之后希望大家能够提出自己的见解。