当修改volatile变量时,会给cpu发送一个信号告诉其他cpu这个变量已修改,当其他cpu调用这个变量时,就会先检查是否有收到修改该变量的信号,有则重新从内存中读取。volatile是无锁的,类似于乐观锁的机制。
在说这个问题之前,我们先看看CPU是如何执行java代码的。首先编译之后Java代码会被编译成字节码.class文件,在运行时会被加载到JVM中,JVM会将.class转换为具体的CPU执行指令,CPU加载这些指令逐条执行。如下图,
![](https://i-blog.csdnimg.cn/blog_migrate/b0c3f259fc2c78d7a1e610a27b90eb62.png)
![](https://i-blog.csdnimg.cn/blog_migrate/e93b441d9e6fce9d5ea106bd207ef0ea.png)
以多核CPU为例(两核),我们知道CPU的速度比内存要快得多,为了弥补这个性能差异,CPU内核都会有自己的高速缓存区,当内核运行的线程执行一段代码时,首先将这段代码的指令集进行缓存行填充到高速缓存,如果非volatil变量当CPU执行修改了此变量之后,会将修改后的值回写到高速缓存,然后再刷新到内存中。如果在刷新会内存之前,由于是共享变量,那么CORE2中的线程执行的代码也用到了这个变量,这是变量的值依然是旧的。volatile关键字就会解决这个问题的,如何解决呢,首先被volatile关键字修饰的共享变量在转换成汇编语言时,会加上一个以lock为前缀的指令,当CPU发现这个指令时,立即做两件事:
-
将当前内核高速缓存行的数据立刻回写到内存;
-
使在其他内核里缓存了该内存地址的数据无效。
第一步很好理解,第二步如何做到呢?使用MESI协议。
MESI协议在早期的CPU中,是通过在总线加LOCK#锁的方式实现的,但这种方式开销太大,所以Intel开发了缓存一致性协议,也就是MESI协议,该解决缓存一致性的大致思路是:当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,那么他会发出信号通知其他CPU将该变量的缓存行设置为无效状态。当其他CPU使用这个变量时,首先会去嗅探是否有对该变量更改的信号,当发现这个变量的缓存行已经无效时,会从新从内存中读取这个变量。
以上这些就是volatile关键字的内部实现机制,使用Volatile有什么好处呢?从底层实现原理我们可以发现,volatile是一种非锁机制,这种机制可以避免锁机制引起的线程上下文切换和调度问题。因此,volatile的执行成本比synchronized更低。
MESI(缓存一致性协议)是一种保持一致性的协议。它的方法是在CPU缓存中保存一个标记位,这个标记位有四种状态:
-
M:Modify,修改缓存,当前CPU的缓存已经被修改了,即与内存中数据已经不一致了
-
E:Exclusive,独占缓存,当前CPU的缓存和内存中数据保持一致,而且其他处理器并没有可使用的缓存数据
-
S:Share,共享缓存,和内存保持一致的一份拷贝,多组缓存可以同时拥有针对同一内存地址的共享缓存段
-
I:Invalid,实效缓存,这个说明CPU中的缓存已经不能使用了
CPU的读取遵循下面几点:
-
如果缓存状态是I,那么就从内存中读取,否则就从缓存中直接读取。
-
如果缓存处于M或E的CPU读取到其他CPU有读操作,就把自己的缓存写入到内存中,并将自己的状态设置为S。
-
只有缓存状态是M或E的时候,CPU才可以修改缓存中的数据,修改后,缓存状态变为M。