重新分配作业并添加围栏

时间:2018-02-12 19:27:06

标签: java variable-assignment volatile memory-fences

以下Java代码看起来有点奇怪,因为我已将其简化为基本要素。我认为代码有一个排序问题。我正在查看JSR-133 Cookbook中的第一个表,看起来普通商店可以使用change()中的易失性存储重新排序。

m_normalchange()的作业可以在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上运行。

2 个答案:

答案 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读取了nullm_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,即可完成设置:volatilem_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_normalget()中读到null。哪个m_volatile写入m_volatile已在get() 中读取 允许 而不是 synchronize-with?这里的问题是允许在m_volatilechange()写入之前的同步操作的总顺序中显示(通过m_normal中的get()重新排序),因此观察null中的初始m_volatile,而synchronize-withm_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()必须在nullm_normal中观察m_volatile。仅当null出现在同步操作的总顺序中的m_volatile之前时,它才会在R1中观察到W2。但这意味着R2必须按此顺序W1之后,因此synchronizes-with。这会在happens-before中读取m_normalget()中的m_normal之间建立change(),因此m_normal读取不允许观察该写入null(无法观察读后发生的写入) - 一个矛盾。因此,m_normalm_volatile读取观察null的原始假设是错误的:至少其中一个会观察到非空值,并且该方法将返回该值。

如果W1中没有get(),则change()中没有任何内容可以强制happens-before读取m_normalm_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不矛盾。