Java volatile环境下JIT生成的x86输出分析

Java volatile环境下JIT生成的x86输出分析,java,jvm,volatile,memory-barriers,Java,Jvm,Volatile,Memory Barriers,我写这篇文章是关于 现在,我正在分析JIT为上述代码生成的内容。从我上一篇文章中的讨论中,我们知道输出1,0是不可能的,因为: 写入volatilev会导致在v之前的每个动作a都会导致a在v之前可见(将刷新到内存) 我是否正确理解它之所以能工作是因为x86无法进行StoreStore重新排序?如果可能的话,它将需要额外的内存屏障,是吗 根据@Eugene的回答进行编辑: 在这里,我明白你的意思了——很清楚:下面的每个动作(之后)volatile read(int tmp=I)不会被重新排序

我写这篇文章是关于

现在,我正在分析JIT为上述代码生成的内容。从我上一篇文章中的讨论中,我们知道输出
1,0
是不可能的,因为:


写入volatile
v
会导致在
v
之前的每个动作
a
都会导致
a
v
之前可见(将刷新到内存)


我是否正确理解它之所以能工作是因为x86无法进行
StoreStore
重新排序?如果可能的话,它将需要额外的内存屏障,是吗


根据@Eugene的回答进行编辑:

在这里,我明白你的意思了——很清楚:
下面的每个动作(之后)
volatile read(
int tmp=I
)不会被重新排序

这里,你再加一道屏障。它确保不会使用
int tmp=i
对任何操作重新排序。但是,为什么它很重要?为什么我有怀疑?据我所知,
volatile load
保证:

在易变负载可见之前,易变负载之后的每个动作都不会被重新排序

我看到你在写:

需要有顺序一致性


但是,我不明白为什么需要顺序一致性。

有几件事,首先
将被刷新到内存中
——这是非常错误的。它几乎从不刷新主内存—它通常会将StoreBuffer消耗到
L1
,所有缓存之间的数据同步取决于缓存一致性协议,但如果您更容易理解这些术语中的概念,那也没关系—只需知道这有点不同,速度更快

这是一个很好的问题,为什么
[StoreLoad]
确实存在,也许这会让事情变得更清楚一点<代码>易失性实际上是关于围栏的,下面是一个示例,说明在一些易失性操作的情况下会插入哪些屏障。例如,我们有一个
易失性负载

  // i is some shared volatile field
  int tmp = i; // volatile load of "i"
  // [LoadLoad|LoadStore]
注意这里的两个屏障
LoadStore
LoadLoad
;在简单的英语中,这意味着任何
负载
存储
易失性负载/读取
之后不能“向上”移动屏障,不能在该易失性负载的“上方”重新排序

下面是
易失性存储
的示例

 // "i" is a shared volatile variable
 // [StoreStore|LoadStore]
 i = tmp; // volatile store
这意味着任何
加载
存储
都不能“低于”加载存储本身

这基本上建立了“先发生后发生”关系,
volatile load
是获取负载,
volatile store
是释放存储(这也与
store
load
cpu缓冲区的实现方式有关,但这几乎超出了问题的范围)

如果你想一想,它对我们所知道的
volatile
一般来说是非常有意义的;它说,一旦易失性负载观察到易失性存储,也会观察到
易失性存储之前的所有内容,这与内存屏障相当。现在,当一个易失性存储发生时,上面的所有内容都不能超出它,并且一旦发生了易失性负载,下面的所有内容都不能超出它,否则之前发生的所有内容都将被破坏

但不是这样,还有更多。需要有顺序一致性,这就是为什么任何sane实现都将保证挥发物本身不会被重新排序,因此又插入了两个围栏:

 // any store of some other volatile
 // can not be reordered with this volatile load
 // [StoreLoad] -- this one
 int tmp = i; // volatile load of a shared variable "i"
 // [LoadStore|LoadLoad]
还有一个:

// [StoreStore|LoadStore]
i = tmp; // volatile store
// [StoreLoad] -- and this one
现在,事实证明在
x86
4个内存屏障中有3个是空闲的,因为它是
强内存模型。唯一需要实现的是
StoreLoad
。在其他CPU上,例如
ARM
lwsyn
是使用的一条指令,但我对它们知之甚少

通常,
mfence
对于
x86
上的
StoreLoad
是一个很好的选择,但是通过
lock add
(以更便宜的方式AFAIK)可以保证这一点,这就是为什么您会在那里看到它。基本上,这就是
StoreLoad
屏障。是的-你在最后一句话中说得对,对于一个较弱的内存模型-需要
StoreStore
屏障。另一方面,当您通过构造函数中的
final
字段安全地发布引用时,会用到这一点。退出构造函数后,将插入两个围栏:
LoadStore
storestorestore

 // "i" is a shared volatile variable
 // [StoreStore|LoadStore]
 i = tmp; // volatile store
恕我直言——JVM可以自由地忽略这些,只要它不违反任何规则:Aleksey Shipilev对此有很好的论述


编辑

假设您有这个案例:

[StoreStore|LoadStore]
int x = 4; // volatile store of a shared "x" variable

int y = 3; // non-volatile store of shared variable "y"

int z = x; // volatile load
[LoadLoad|LoadStore]
基本上没有任何屏障可以阻止
易失性存储
易失性负载
一起重新订购(即:首先执行易失性负载),这将导致明显的问题;因此违反了顺序一致性

顺便说一句(如果我没有弄错的话),在volatile load之后的每个操作在volatile load可见之前都不会被重新排序。volatile本身不可能重新排序-其他操作可以自由重新排序。让我举个例子:

 int tmp = i; // volatile load of a shared variable "i"
 // [LoadStore|LoadLoad]

 int x = 3; // plain store
 int y = 4; // plain store
最后两个操作
x=3
y=4
完全可以自由重新排序,它们不能浮在volatile之上,但它们可以通过自身重新排序。上述例子完全合法:

 int tmp = i; // volatile load
 // [LoadStore|LoadLoad]

 // see how they have been inverted here...
 int y = 4; // plain store
 int x = 3; // plain store

有几件事,首先
将被刷新到内存中
——这是非常错误的。它几乎从不刷新主内存-它通常会将StoreBuffer消耗到
L1
,所有缓存之间的数据同步取决于缓存一致性协议,但如果您更容易理解这一概念,请参阅
[StoreStore|LoadStore]
int x = 4; // volatile store of a shared "x" variable

int y = 3; // non-volatile store of shared variable "y"

int z = x; // volatile load
[LoadLoad|LoadStore]
 int tmp = i; // volatile load of a shared variable "i"
 // [LoadStore|LoadLoad]

 int x = 3; // plain store
 int y = 4; // plain store
 int tmp = i; // volatile load
 // [LoadStore|LoadLoad]

 // see how they have been inverted here...
 int y = 4; // plain store
 int x = 3; // plain store