Java发生指令重排的几种情况

  1. JIT编译优化
  2. CPU自身优化
  3. store buffers下会导致的看似“重排序”的现象

volatile对指令重排影响的几种假设

首先,volatile是java语言层面给出的保证,MSEI协议是多核cpu保证cache一致性的一种方法,中间隔的还很远,我们可以先来做几个假设:

1. 单核或多核顺序执行

回到远古时候,那个时候cpu只有单核,或者是多核但是保证sequence consistency,当然也无所谓有没有MESI协议了。那这个时候,我们需要java语言层面的volatile的支持吗?

当然是需要的。 因为在语言层面编译器和虚拟机为了做性能优化,可能会存在指令重排的可能,而volatile给我们提供了一种能力,我们可以告诉编译器,什么可以重排,什么不可以。

2. 假设Java语言层面不进行指令重排优化

那好,假设更进一步,假设java语言层面不会对指令做任何的优化重排,那在多核cpu的场景下,我们还需要volatile关键字吗?

答案仍然是需要的。 因为 MESI只是保证了多核cpu的独占cache之间的一致性,但是cpu的并不是直接把数据写入L1 cache的,中间还可能有store buffer。有些arm和power架构的cpu还可能有load buffer或者invalid queue等等。因此,有MESI协议远远不够。

3. CPU直接读写cache

再接着,让我们再做一个更大胆的假设。假设cpu中这类store buffer/invalid queue等等都不存在了,cpu是数据是直接写入cache的,读取也是直接从cache读的,那还需要volatile关键字吗?

你猜的没错,还需要的。原因就在这个“一致性”上。 consistency和coherence都可以被翻译为一致性,但是MSEI协议这里保证的仅仅coherence而不是consistency。那consistency和cohence有什么区别呢? Coherence deals with maintaining a global order in which writes to a single location or single variable are seen by all processors. Consistency deals with the ordering of operations to multiple locations with respect to all processors. 因此,MESI协议最多只是保证了对于一个变量,在多个核上的读写顺序,对于多个变量而言是没有任何保证的。很遗憾,还是需要volatile。

4. CPU按照指令顺序写cache

好的,到了现在这步,我们再来做最后一个假设,假设cpu写cache都是按照指令顺序FIFO写的,那现在可以抛弃volatile了吧?

你觉得呢?都已经第四个假设了,那肯定不行啊! 因为对于arm和power这个weak consistency的架构的cpu来说,它们只会保证指令之间有比如控制依赖,数据依赖,地址依赖等等依赖关系的指令间提交的先后顺序,而对于完全没有依赖关系的指令。 比如x=1;y=2;它们是不会保证执行提交的顺序的,除非你使用了volatile,java把volatile编译成arm和power能够识别的barrier指令,这个时候才是按顺序的。

总结volatile的作用

  1. 禁止编译器和cpu优化向的指令重排序
    1. 在没有volatile的情况下,以上假设2、3都会有第1、2两种重排序的困扰,这时候就需要volatile通过读写屏障,告知编译器和cpu,禁止单核心的指令重排
  2. 使得写核心store buffers的值能立即写入缓存行,其他共享核心能立即处理失效请求(清空无效化队列),避免多核心操作多个变量导致的看似“重排序”情况。