Java内存模型和重新排序操作

标签 java memory-barriers java-memory-model

我的问题是针对帖子: https://shipilev.net/blog/2014/safe-public-construction/

public class UnsafeDCLFactory {
  private Singleton instance;

  public Singleton get() {
    if (instance == null) {  // read 1, check 1
      synchronized (this) {
        if (instance == null) { // read 2, check 2
          instance = new Singleton(); // store
        }
      }
    }
    return instance; // read 3
  }
}

而且,它是这样写的:

Notice that we do several reads of instance in this code, and at least "read 1" and "read 3" are the reads without any synchronization — that is, those reads are racy. One of the intents of the Java Memory Model is to allow reorderings for ordinary reads, otherwise the performance costs would be prohibitive. Specification-wise, as mentioned in happens-before consistency rules, a read action can observe the unordered write via the race. This is decided for each read action, regardless what other actions have already read the same location. In our example, that means that even though "read 1" could read non-null instance, the code then moves on to returning it, then it does another racy read, and it can read a null instance, which would be returned!

我看不懂。我同意编译器显然可以重新排序内存操作。但是,这样做时,编译器必须从单线程的角度保留原始程序的行为。

在上面的例子中,read 1 读到了非空值。 read 3 读取 null。这意味着 read 3 被编译器重新排序并以 read 1 的优先级读取 instance (我们可以跳过 CPU 重新排序,因为帖子提出了 Java 内存模型)。

但是,在我看来 read 3 不能超过 read 1 因为存储操作 - 我的意思是 instance = new Singleton();/p>

毕竟存在数据依赖,这意味着编译器无法将指令read 3重新排序为store,因为它改变了程序的含义(即使是单线程).编译器也不能更改 read 1 的顺序,因为它必须在 store 之前。否则,单线程程序的语义不同。

因此,顺序必须是:读1 -> 存储->读3

你怎么看?

附言发表一些东西是什么意思?特别是,不安全地发布内容是什么意思?


这是对@Aleksey Shipilev 回答的重新回答。

Let me say this again -- failure to construct the example does not disprove the rule.

是的,这是显而易见的。

And Java Memory Model allows returning null on the second read.

我也同意这一点。我不声称它不允许。 (这可能是因为数据竞争——是的,它们是邪恶的)。我声称 read 3 不能超过 read 1。我知道你是对的,但我想明白这一点。我仍然声称 Java 编译器无法生成 read 3 接管 read 1 的字节码。我看到 read3 可以读取 null 因为数据竞争,但我无法想象 read 1 读取非 null 和 read 3 read null 而 read 3 由于数据依赖无法 catch read 1

(我们不考虑硬件(CPU)级别的内存排序)

最佳答案

But, doing it, the compiler has to preserve the behaviuor of the original program from the one-thread's point view.

没有。它必须保留语言规范的要求。在这种情况下,JMM。如果某些转换在 JMM 下是合法的,那么就可以执行它。 “单线程的观点”不是规范语言,规范才是。

Java 内存模型允许在第二次读取时返回null。如果您无法构造执行此操作的实际转换,并不意味着这种转换是不可能的。让我再说一遍——未能构建示例并不反驳规则。现在您可以在 "Benign Data Races are Resilient" 中看到示例转换, 并在那里阅读这一段:

This may appear counter-intuitive: if we read null from instance, we take a corrective action with storing new instance, and that’s it. Indeed, if we have the intervening store to instance, we cannot see the default value, and we can only see that store (since it happens-before us in all conforming executions where first read returned null), or the store from the other thread (which is not null, just a different object). But the interesting behavior unfolds when we don’t read null from the instance on the first read. No intervening store takes place. The second read tries to read again, and being racy as it is, may read null. Ouch.

也就是说,您可以轻松地转换程序以公开路径而无需干预写入,并且微不足道的读取重新排序会产生“违反直觉”的结果。字节码并不是程序可以接受的唯一“转换”。上面的链接概述了编译器转换,在存储上公开代码路径而不依赖数据。 IE。这两个程序略有不同:

// Case 1
Object o1 = instance;
instance = new Object();
Object o2 = instance;

// Case 2
Object o1 = instance;
if (o1 == null)
  instance = new Object();
Object o2 = instance;

在“情况 2”中,一条避免存储到 instance 的路径——因为由于分支,程序现在有两条路径——优化转换可以暴露它。

这是一个可能出错的人为示例。在实际程序中,控制和数据流要复杂得多,并且允许优化转换(并且最终会)产生类似的结果,因为它们在一组派生规则上运行,如“无同步,无数据依赖 - 自由移动东西”,经过大量转换以暴露有趣的行为。

数据竞赛是邪恶的。

关于Java内存模型和重新排序操作,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/43848870/

相关文章:

创建数组时出现 Java 空指针异常

c# - 流行的 "volatile polled flag"模式坏了吗?

c++ - 如何为指令重新排序编写可观察的示例?

java - 这种非标准的 Java 同步模式有效吗?

java - 读取新值后读取旧值

java - 如何在android中下载页面

java - 保存 XDrawPage,获取全屏演示文稿

java - 如何用一个对象作为值来创建 HashMap,它又拥有自己的对象 HashMap。 java

x86 - 断点在宽松内存模型上的行为

java - 为什么要对两个 volatile 变量进行重新排序?