重新排序分配并添加围栏

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 观察 nullm_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_normalm_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_normalvolatile,大家都会睡得更好。

// 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_normalm_volatile 中的 null。只有当R1在同步动作的总顺序中出现在W2之前时,它才能在m_volatile中观察到null。但这意味着 R2 必须按顺序在 W1 之后,因此 synchronizes-with 它。这在 get() 中读取的 m_normalchange() 中写入的 m_normal 之间建立了一个 happens-before,因此不允许 m_normal 读取观察到该写入null(无法观察读取后发生的写入)-矛盾。所以最初假设 m_normalm_volatile 都读取 observe null 是错误的:至少其中一个会观察 non-null 值,并且该方法将 return那。

如果 get() 中没有 W1change() 中没有任何内容可以强制 m_normal 读取和读取之间的 happens-before 边缘m_normal 写 - 因此,观察 get() 中的写并不与 JMM 相矛盾。