volatile作用与处理器嗅探的简解
先贴一下 volatile 的作用定义
如果一个字段被声明成volatile,Java线程内存模型确保所有线程看到这个变量的值是一致的
首先问题就来了,一个共享变量再被volatile修饰过后,怎么被确保所有线程看到的这个变量的值是一致的的呢,也就是说volatile是如何来保证可见性的呢?
在X86处理器下通过工具获取JIT编译器生成的汇编指令来查看对volatile进行写操作时,CPU会做什么事情。
private volatile instance = new Singleton();
转变成汇编代码,如下。
0x01a3de1d: movb $0×0,0×1104800(%esi);
0x01a3de24: lock addl $0×0,(%esp);
有 volatile 变量修饰的共享变量进行写操作的时候会多出第二行汇编代码,通过查IA-32架构软件开发者手册可知,Lock前缀的指令在多核处理器下会引发了两件事情。
1)将当前处理器缓存行的数据写回到系统内存。
2)这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效。
第一件事很容易理解,处理器在修改数据后通常都是先写入到缓存中,但是并不会第一时间写回到主内存中。
被 volatile 修饰后,这个变量被操作后会立即被写回到主内存中。(当然整个过程会比较复杂,但是我们只需要从结果上来看和简化理解就OK了。)
那第二件事中这个写回内存的操作是如何使其他CPU里缓存了该内存地址的数据无效的呢?
为了提高处理速度,处理器不直接和内存进行通信,而是先将系统内存的数据读到内部缓存(L1,L2或其他)后再进行操作,但操作完不知道何时会写到内存。
如果对声明了volatile的变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存。
但是,就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操作就会有问题。
所以,在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,
当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里。
这么看来的话,各处理器之间是通过实现缓存一致性协议来完成第二件事的。
那么“每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了”,这里的处理器是如何嗅探的呢?
我百度了半天也没有单独将如何嗅探的,但是看了一遍博文后(并发研究之CPU缓存一致性协议(MESI)) ,我有了一些自己的理解,当然也只是为了简化复杂流程的简化理解。
image以上是目前流行的多级缓存结构简化图,
处理器无论是想要加载数据或者写回数据,都需要通过总线(图中的②bus)来传播,
那么我们也就可以将 “处理器通过嗅探在总线上传播的数据” 这样的操作形象的理解为处理器监听 总线 上的所有修改操作,
当处理器发现自己的缓存中的某个数据在总线上被其他的处理器修改了,那么就将自己缓存中的这个数据的状态变成无效状态,
然后当处理器在处理这个无效状态的数据时,会重新去主内存中加载这个数据,然后在进行相应的操作。
比如:CPU A的cache a 已经缓存了 x,然后 CPU B的cache b 也已经缓存了 x,这时 CPU A要修改 x 的值,
然后先将修改后的数据写回到主内存中,在写回到主内存的同时被CPU B嗅探到了,并且发现这个数据在自己的cache b中也存在,然后CPU B就先将自己cache b中的 x 的状态设置成无效,
当CPU B处理到 x 时,发现 x 的状态是无效的,就只能先去主内存中重新加载 x 的值后再操作。
以上便是个人对于处理器嗅探操作的简化理解,虽然简化理解后的流程顺序和原本的流程顺序有所出入,但是这样简化理解只是为了方便自己理解和记忆。
需要了解具体的整体操作流程的话,可以去看上面提到的那边博文。
网友评论