重新排序分配并添加围栏
Re-ordering of assignments and adding a fence
以下 Java 代码看起来有点奇怪,因为我已将其简化为最基本的部分。我认为代码有排序问题。我正在查看 JSR-133 Cookbook 中的第一个 table,似乎可以使用 change()
.
中的易失性存储对普通存储进行重新排序
change()
中对 m_normal
的赋值是否可以先于 m_volatile
的赋值?也就是说,可以get()
return null
?
解决这个问题的最佳方法是什么?
private Object m_normal = new Object();
private volatile Object m_volatile;
public void change() {
Object normal;
normal = m_normal; // Must capture value to avoid double-read
if (normal == null) {
return;
}
m_volatile = normal;
m_normal = null;
}
public Object get() {
Object normal;
normal = m_normal; // Must capture value to avoid double-read
if (normal != null) {
return normal;
}
return m_volatile;
}
注意:我无法控制声明 m_normal
的代码。
注意:我是 运行 Java 8.
TL;DR: 朋友们不要让朋友们浪费时间弄清楚活泼的访问是否符合 Extreme Concurrency Optimist 的期望。使用volatile
,睡个好觉
I am looking at the first table in the JSR-133 Cookbook
请注意完整标题是 "JMM Cookbook For Compiler Writers"。这引出了一个问题:我们是编译器编写者,还是只是试图弄清楚我们代码的用户?我认为是后者,所以我们真的应该关闭 JMM Cookbook,并打开 JLS 本身。请参阅 "Myth: JSR 133 Cookbook Is JMM Synopsis" 和之后的部分。
In other words, can get() return null?
是的,通过 get()
观察字段的默认值,而不观察 change()
所做的任何事情。 :)
但我想问题是,在 change()
完成后是否允许在 m_volatile
中看到旧值(警告:对于 "completed" 的某些概念,因为这意味着时间,逻辑时间由JMM自己指定。
问题基本上是,是否有包含 read(m_normal):null --po/hb--> read(m_volatile):null
的有效执行,读取 m_normal
观察 null
到 m_normal
的写入?是的,这里是:write(m_volatile, X) --po/hb--> write(m_normal, null) ... read(m_normal):null --po/hb--> read(m_volatile):null
.
对m_normal
的读取和写入没有顺序,因此不存在禁止读取两个空值的执行的结构约束。但是"volatile",你会说!是的,它有一些限制,但它的顺序是错误的 w.r.t。 non-volatile 操作,请参阅 "Pitfall: Acquiring and Releasing in Wrong Order"(仔细查看该示例,它与您的要求非常相似)。
确实 m_volatile
上的操作本身提供了一些内存语义:对 m_volatile
的写入是 "release" "publishes" 一切都发生在它之前,并且来自 m_volatile
的阅读是 "acquire" "gets" 发布的所有内容。如果你像这样 post 准确地进行推导,就会出现模式:你可以 平凡地 将操作移动到 "release" program-upwards (那些是反正很活泼!),你可以 简单地 将操作移到 "acquire" program-downwards 上(无论如何也很活泼!)。
这种解释经常被称为 "roach motel semantics",并给出了直观的答案:"Can these two statements reorder?"
m_volatile = value; // release
m_normal = null; // some other store
roach motel 语义下的答案是 "yes"。
What is the best way to solve this?
最好的解决方法是从一开始就避免不正当的操作,从而避免整个混乱。只需使 m_normal
volatile
,一切就绪:对 m_normal
和 m_volatile
的操作将顺序一致。
Would adding value = m_volatile; after m_volatile = value; prevent the assignment of m_normal happening before the assignment of m_volatile?
所以问题是,这是否有帮助:
m_volatile = value; // "release"
value = m_volatile; // poison "acquire" read
m_normal = null; // some other store
在 只有 roach motel 语义的天真世界中,它可能会有所帮助:似乎毒药获取破坏了代码移动。但是,由于该读取的值未被观察到,它等同于没有任何有害读取的执行,优秀的优化器会利用它。参见 "Wishful Thinking: Unobserved Volatiles Have Memory Effects"。重要的是要理解挥发性并不总是意味着障碍,即使 JMM Cookbook for Compiler Writers 中概述的 conservative 实现有它们。
旁白:还有一个替代方案,VarHandle.fullFence()
可以在这样的示例中使用,但它仅限于非常强大的用户,因为有障碍的推理会变得疯狂。参见 "Myth: Barriers Are The Sane Mental Model" and "Myth: Reorderings And Commit to Memory"。
只要m_normal
volatile
,大家都会睡得更好。
// Must capture value to avoid double-read
亵渎。编译器可以通过正常访问自由地执行它喜欢的操作,在没有 Java 代码执行时重复它们,在有 Java 代码执行时消除它们 - 任何不破坏 Java语义。
在这两者之间插入易失性读取:
m_volatile = normal;
tmp = m_volatile; // "poison read"
m_normal = null;
不正确的原因与 Aleksey Shipilev 在他的回答中所说的不同:JMM 对修改 订单 此类操作的陈述为零;消除未观察到的 "poison read" 永远不会修改任何操作的顺序(永远不会消除障碍)。 "poison read"
的实际问题在 get()
.
假设,m_normal
读入 get()
观察 null
。其中 m_volatile
写入是 m_volatile
读取 get()
not allowed to 不是 synchronize-with
?这里的问题是它允许出现在 m_volatile
写入 change()
之前的同步操作的总顺序中(用 m_normal
读取 get()
重新排序),因此观察m_volatile
中的初始 null
,而不是 synchronize-with
写入 change()
中的 m_volatile
。在 m_volatile
读入 get()
之前,您需要一个 "full barrier" - 一个不稳定的存储。你不想要的。
此外,仅在 change()
中使用 VarHandle.fullFence()
并不能解决问题,原因相同:get()
中的种族并没有因此而被淘汰。
PS。 Aleksey 在 https://shipilev.net/blog/2016/close-encounters-of-jmm-kind/#wishful-unobserved-volatiles 幻灯片上给出的解释不正确。那里没有消失的障碍,只允许访问 GREAT_BARRIER_REEF
分别作为第一个和最后一个同步操作出现的部分顺序。
您应该从允许 get()
到 return null
的假设开始。然后建设性地证明这是不允许的。在你有这样的证据之前,你应该假设它仍然会发生。
您可以建设性地证明 null
不被允许的示例:
volatile boolean m_v;
volatile Object m_volatile;
Object m_normal = new Object();
public void change() {
Object normal;
normal = m_normal;
if (normal == null) {
return;
}
m_volatile = normal; // W2
boolean v = m_v; // R2
m_normal = null;
}
public Object get() {
Object normal;
normal = m_normal;
if (normal != null) {
return normal;
}
m_v = true; // W1
return m_volatile; // R1
}
现在,从假设 get()
可能 return null
开始。为此,get()
必须遵守 m_normal
和 m_volatile
中的 null
。只有当R1
在同步动作的总顺序中出现在W2
之前时,它才能在m_volatile
中观察到null
。但这意味着 R2
必须按顺序在 W1
之后,因此 synchronizes-with
它。这在 get()
中读取的 m_normal
和 change()
中写入的 m_normal
之间建立了一个 happens-before
,因此不允许 m_normal
读取观察到该写入null
(无法观察读取后发生的写入)-矛盾。所以最初假设 m_normal
和 m_volatile
都读取 observe null
是错误的:至少其中一个会观察 non-null 值,并且该方法将 return那。
如果 get()
中没有 W1
,change()
中没有任何内容可以强制 m_normal
读取和读取之间的 happens-before
边缘m_normal
写 - 因此,观察 get()
中的写并不与 JMM 相矛盾。
以下 Java 代码看起来有点奇怪,因为我已将其简化为最基本的部分。我认为代码有排序问题。我正在查看 JSR-133 Cookbook 中的第一个 table,似乎可以使用 change()
.
change()
中对 m_normal
的赋值是否可以先于 m_volatile
的赋值?也就是说,可以get()
return null
?
解决这个问题的最佳方法是什么?
private Object m_normal = new Object();
private volatile Object m_volatile;
public void change() {
Object normal;
normal = m_normal; // Must capture value to avoid double-read
if (normal == null) {
return;
}
m_volatile = normal;
m_normal = null;
}
public Object get() {
Object normal;
normal = m_normal; // Must capture value to avoid double-read
if (normal != null) {
return normal;
}
return m_volatile;
}
注意:我无法控制声明 m_normal
的代码。
注意:我是 运行 Java 8.
TL;DR: 朋友们不要让朋友们浪费时间弄清楚活泼的访问是否符合 Extreme Concurrency Optimist 的期望。使用volatile
,睡个好觉
I am looking at the first table in the JSR-133 Cookbook
请注意完整标题是 "JMM Cookbook For Compiler Writers"。这引出了一个问题:我们是编译器编写者,还是只是试图弄清楚我们代码的用户?我认为是后者,所以我们真的应该关闭 JMM Cookbook,并打开 JLS 本身。请参阅 "Myth: JSR 133 Cookbook Is JMM Synopsis" 和之后的部分。
In other words, can get() return null?
是的,通过 get()
观察字段的默认值,而不观察 change()
所做的任何事情。 :)
但我想问题是,在 change()
完成后是否允许在 m_volatile
中看到旧值(警告:对于 "completed" 的某些概念,因为这意味着时间,逻辑时间由JMM自己指定。
问题基本上是,是否有包含 read(m_normal):null --po/hb--> read(m_volatile):null
的有效执行,读取 m_normal
观察 null
到 m_normal
的写入?是的,这里是:write(m_volatile, X) --po/hb--> write(m_normal, null) ... read(m_normal):null --po/hb--> read(m_volatile):null
.
对m_normal
的读取和写入没有顺序,因此不存在禁止读取两个空值的执行的结构约束。但是"volatile",你会说!是的,它有一些限制,但它的顺序是错误的 w.r.t。 non-volatile 操作,请参阅 "Pitfall: Acquiring and Releasing in Wrong Order"(仔细查看该示例,它与您的要求非常相似)。
确实 m_volatile
上的操作本身提供了一些内存语义:对 m_volatile
的写入是 "release" "publishes" 一切都发生在它之前,并且来自 m_volatile
的阅读是 "acquire" "gets" 发布的所有内容。如果你像这样 post 准确地进行推导,就会出现模式:你可以 平凡地 将操作移动到 "release" program-upwards (那些是反正很活泼!),你可以 简单地 将操作移到 "acquire" program-downwards 上(无论如何也很活泼!)。
这种解释经常被称为 "roach motel semantics",并给出了直观的答案:"Can these two statements reorder?"
m_volatile = value; // release
m_normal = null; // some other store
roach motel 语义下的答案是 "yes"。
What is the best way to solve this?
最好的解决方法是从一开始就避免不正当的操作,从而避免整个混乱。只需使 m_normal
volatile
,一切就绪:对 m_normal
和 m_volatile
的操作将顺序一致。
Would adding value = m_volatile; after m_volatile = value; prevent the assignment of m_normal happening before the assignment of m_volatile?
所以问题是,这是否有帮助:
m_volatile = value; // "release"
value = m_volatile; // poison "acquire" read
m_normal = null; // some other store
在 只有 roach motel 语义的天真世界中,它可能会有所帮助:似乎毒药获取破坏了代码移动。但是,由于该读取的值未被观察到,它等同于没有任何有害读取的执行,优秀的优化器会利用它。参见 "Wishful Thinking: Unobserved Volatiles Have Memory Effects"。重要的是要理解挥发性并不总是意味着障碍,即使 JMM Cookbook for Compiler Writers 中概述的 conservative 实现有它们。
旁白:还有一个替代方案,VarHandle.fullFence()
可以在这样的示例中使用,但它仅限于非常强大的用户,因为有障碍的推理会变得疯狂。参见 "Myth: Barriers Are The Sane Mental Model" and "Myth: Reorderings And Commit to Memory"。
只要m_normal
volatile
,大家都会睡得更好。
// Must capture value to avoid double-read
亵渎。编译器可以通过正常访问自由地执行它喜欢的操作,在没有 Java 代码执行时重复它们,在有 Java 代码执行时消除它们 - 任何不破坏 Java语义。
在这两者之间插入易失性读取:
m_volatile = normal;
tmp = m_volatile; // "poison read"
m_normal = null;
不正确的原因与 Aleksey Shipilev 在他的回答中所说的不同:JMM 对修改 订单 此类操作的陈述为零;消除未观察到的 "poison read" 永远不会修改任何操作的顺序(永远不会消除障碍)。 "poison read"
的实际问题在 get()
.
假设,m_normal
读入 get()
观察 null
。其中 m_volatile
写入是 m_volatile
读取 get()
not allowed to 不是 synchronize-with
?这里的问题是它允许出现在 m_volatile
写入 change()
之前的同步操作的总顺序中(用 m_normal
读取 get()
重新排序),因此观察m_volatile
中的初始 null
,而不是 synchronize-with
写入 change()
中的 m_volatile
。在 m_volatile
读入 get()
之前,您需要一个 "full barrier" - 一个不稳定的存储。你不想要的。
此外,仅在 change()
中使用 VarHandle.fullFence()
并不能解决问题,原因相同:get()
中的种族并没有因此而被淘汰。
PS。 Aleksey 在 https://shipilev.net/blog/2016/close-encounters-of-jmm-kind/#wishful-unobserved-volatiles 幻灯片上给出的解释不正确。那里没有消失的障碍,只允许访问 GREAT_BARRIER_REEF
分别作为第一个和最后一个同步操作出现的部分顺序。
您应该从允许 get()
到 return null
的假设开始。然后建设性地证明这是不允许的。在你有这样的证据之前,你应该假设它仍然会发生。
您可以建设性地证明 null
不被允许的示例:
volatile boolean m_v;
volatile Object m_volatile;
Object m_normal = new Object();
public void change() {
Object normal;
normal = m_normal;
if (normal == null) {
return;
}
m_volatile = normal; // W2
boolean v = m_v; // R2
m_normal = null;
}
public Object get() {
Object normal;
normal = m_normal;
if (normal != null) {
return normal;
}
m_v = true; // W1
return m_volatile; // R1
}
现在,从假设 get()
可能 return null
开始。为此,get()
必须遵守 m_normal
和 m_volatile
中的 null
。只有当R1
在同步动作的总顺序中出现在W2
之前时,它才能在m_volatile
中观察到null
。但这意味着 R2
必须按顺序在 W1
之后,因此 synchronizes-with
它。这在 get()
中读取的 m_normal
和 change()
中写入的 m_normal
之间建立了一个 happens-before
,因此不允许 m_normal
读取观察到该写入null
(无法观察读取后发生的写入)-矛盾。所以最初假设 m_normal
和 m_volatile
都读取 observe null
是错误的:至少其中一个会观察 non-null 值,并且该方法将 return那。
如果 get()
中没有 W1
,change()
中没有任何内容可以强制 m_normal
读取和读取之间的 happens-before
边缘m_normal
写 - 因此,观察 get()
中的写并不与 JMM 相矛盾。