volatile关键字和cpu缓存一致性协议

8yparm6h  于 2021-07-08  发布在  Java
关注(0)|答案(1)|浏览(398)

cpu已经通过一些协议(比如mesi)保证了缓存的一致性。为什么我们还需要 volatile 在某些语言(如java)中保持多线程之间的可见性。
可能的原因是这些协议在引导时没有启用,必须由一些指令触发,比如 LOCK .
如果真是这样,为什么启动时cpu不启用协议?

jutyujz0

jutyujz01#

volatile可防止两种不同的问题:
能见度
重新排序
我假设x86。。
首先,x86上的缓存总是一致的。因此,在一个cpu将某个变量的存储提交到缓存之后,另一个cpu仍然会加载该变量的旧值,这是不可能的。这是mesi协议的域。
假设java字节码中的每一个put和get都被翻译(而不是优化)到一个存储和cpu上的一个负载,那么即使没有volatile,每一个get都会看到同一个地址上最近的put。
这里的问题是编译器(在本例中是jit)在优化代码方面有很大的自由度。例如,如果它检测到在循环中读取了相同的字段,它可以决定将该变量从循环中提升出来,如下所示。

for(...){
       int tmp = a;
       println(tmp);
 }

吊装后:

int tmp = a;
 for(...){
       println(tmp);
 }

如果该字段仅被1个线程触及,则这是很好的。但是如果字段被另一个线程更新,第一个线程将永远看不到更改。使用volatile可以防止此类可见性问题,这实际上是:
c型挥发性
jsr-133引入java内存模型之前的java volatile。
具有不透明访问模式的varhandle。
还有另一个非常重要的方面就是易变性;volatile防止对某些cpu执行的指令流中不同地址的加载和存储进行重新排序。jit编译器和cpu有很大的自由来重新排序加载和存储。尽管在x86上,由于存储缓冲区的原因,只有较旧的存储可以用较新的加载重新排序到不同的地址。
想象一下下面的代码:

int a;
volatile int b;

thread1:
    a=1;
    b=1;

thread2:
    if(b==1) print(a);

事实上 b 是挥发性的防止储存 a=1 在商店后面跳 b=1 . 它还可以防止 a 在…之前跳进去 b . 这样线程2就可以看到 a=1 ,当它读到 b=1 .
因此,使用volatile,可以确保非volatile字段对其他线程可见。
如果您想了解volatile是如何工作的,我建议您深入研究java内存模型,它是用synchronize with表示的,发生在margeret bloom已经指出的规则之前。我已经给出了一些低级的细节,但是对于java,最好使用这个高级模型,而不是从硬件的Angular 来考虑。仅从硬件/围栏的Angular 考虑,这只适用于Maven,不可携带且非常脆弱。

相关问题