以下Java代码看起来有点奇怪,因为我已将其简化为基本要素。我认为代码有一个排序问题。我正在查看JSR-133 Cookbook中的第一个表,看起来普通商店可以使用change()
中的易失性存储重新排序。
m_normal
中change()
的作业可以在m_volatile
的作业之前移动吗?换句话说,get()
可以返回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上运行。
答案 0 :(得分:20)
TL; DR:朋友不要让朋友浪费时间搞清楚racy访问是否符合Extreme Concurrency Optimist的要求。使用volatile
,愉快地睡觉。
我正在查看JSR-133 Cookbook中的第一个表
请注意完整标题是“JMM Cookbook For Compiler Writers ”。这引出了一个问题:我们在这里编写编写器,还是只是试图找出我们代码的用户?我认为后者,所以我们应该关闭JMM Cookbook,并打开JLS本身。请参阅"Myth: JSR 133 Cookbook Is JMM Synopsis"以及之后的部分。
换句话说,get()可以返回null吗?
是的,通过get()
观察字段的默认值,而不观察change()
所做的任何事情。 :)
但我想问题是,在m_volatile
完成之后,是否允许在change()
中查看旧值(警告:对于某些“已完成”的概念,因为这意味着时间和逻辑时间由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
的读取和写入不是,因此没有结构约束禁止执行读取两个空值。但你会说“挥发”!是的,它带有一些限制,但它的顺序是错误的w.r.t.非易失性操作,请参阅"Pitfall: Acquiring and Releasing in Wrong Order"(仔细查看该示例,它与您要求的非常相似)。
m_volatile
本身的操作确实提供了一些内存语义:对m_volatile
的写入是“发布”,“发布”它之前发生的一切,以及{{1}的读取是“获取”“获取”发布的所有内容。如果你准确地完成了这篇文章中的派生,那么模式会显示出来:你可以平凡地将操作移到“释放”程序向上(无论如何都是racy!),你可以平凡地将操作移到“获取”程序 - 向下移动(无论如何也是这样的!)。
这种解释经常被称为"roach motel semantics",并给出直观的答案:“这两个陈述可以重新排序吗?”
m_volatile
蟑螂汽车旅行语义下的答案是“是”。
解决此问题的最佳方法是什么?
解决问题的最佳方法是避免开始操作,从而避免整个混乱。只需设置m_volatile = value; // release
m_normal = null; // some other store
m_normal
,即可完成设置:volatile
和m_normal
上的操作将按顺序保持一致。
会增加值= m_volatile;在m_volatile = value之后;在分配m_volatile之前阻止m_normal的分配?
所以问题是,这会有所帮助:
m_volatile
在仅蟑螂汽车旅馆语义的天真世界中,它可能有所帮助:似乎毒药获取会打破代码运动。但是,由于该读取的值未被观察到,因此它等同于没有任何毒性读取的执行,并且优秀的优化器会利用它。见"Wishful Thinking: Unobserved Volatiles Have Memory Effects"。重要的是要理解挥发性并不总是意味着障碍,即使JMM Cookbook for Compiler Writers中列出的保守实现也具有这些障碍。
除此之外:还有一个替代方案,m_volatile = value; // "release"
value = m_volatile; // poison "acquire" read
m_normal = null; // some other store
可以在这样的示例中使用,但它仅限于非常强大的用户,因为有障碍的推理会出现边缘疯狂。请参阅"Myth: Barriers Are The Sane Mental Model"和"Myth: Reorderings And Commit to Memory"。
只需制作VarHandle.fullFence()
m_normal
,每个人都会睡得更好。
答案 1 :(得分:-1)
// 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"
的实际问题出在get()
。
假设m_normal
在get()
中读到null
。哪个m_volatile
写入m_volatile
已在get()
中读取 允许 而不是 synchronize-with
?这里的问题是允许在m_volatile
中change()
写入之前的同步操作的总顺序中显示(通过m_normal
中的get()
重新排序),因此观察null
中的初始m_volatile
,而synchronize-with
中m_volatile
的{{1}}写入change()
。在m_volatile
读入get()
之前,您需要一个“完全障碍” - 一个易变的商店。你不想要的。
此外,单独使用VarHandle.fullFence()
中的change()
并不会出于同样的原因解决问题:get()
中的竞赛并未被消除。
PS。 Aleksey对https://shipilev.net/blog/2016/close-encounters-of-jmm-kind/#wishful-unobserved-volatiles幻灯片的解释是不正确的。那里没有消失的障碍,只允许部分订单,其中GREAT_BARRIER_REEF
的访问分别显示为第一个和最后一个同步操作。
您应该首先假设允许get()
返回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()
可能会返回null
。为此,get()
必须在null
和m_normal
中观察m_volatile
。仅当null
出现在同步操作的总顺序中的m_volatile
之前时,它才会在R1
中观察到W2
。但这意味着R2
必须按此顺序W1
之后,因此synchronizes-with
。这会在happens-before
中读取m_normal
与get()
中的m_normal
之间建立change()
,因此m_normal
读取不允许观察该写入null
(无法观察读后发生的写入) - 一个矛盾。因此,m_normal
和m_volatile
读取观察null
的原始假设是错误的:至少其中一个会观察到非空值,并且该方法将返回该值。
如果W1
中没有get()
,则change()
中没有任何内容可以强制happens-before
读取m_normal
与m_normal
之间的get()
边缘1}}写 - 因此,观察struct Action { var timestamp: TimeInterval; var winner: String }
var actions: [Action] = []
...
actions.map({ ["time": $0.timestamp, "winner": $0.winner ] as NSDictionary })
// You could also convert the actions manually one by one into NSDictionary
// and append them to an [NSDictionary]
UserDefaults.standard.set(actions as NSArray, forKey: "currentMatch")
UserDefaults.standard.synchronize()
中的写入与JMM不矛盾。