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

Java Memory Model and reordering operation

我的问题是针对 post: 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 读取为空。这意味着 read 3 被编译器重新排序并读取优先于 read 1instance (我们可以跳过 CPU 重新排序,因为 post 引发 Java内存模型)。

但是,在我看来read 3不能超过read 1,因为商店运营-我的意思是instance = new Singleton();

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

因此,顺序必须是:read 1 -> store -> read 3

你怎么看?

P.S。发表一些东西是什么意思?特别是,不安全地发布内容是什么意思?


这是对@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 读取 null 而 read 3 由于数据依赖性无法超越 read 1

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

考虑 class

class Foo { String bar = "qux"; }

当阅读 Foo::bar 时,您总是希望它 return 字符串 "qux"。然而,构造函数调用不是原子的。它可以分为两部分:

Foo foo = <init> Foo;
foo.bar = "qux";

使用不安全发布,线程可能已发布 Foo 但未从其本地内存发布其值 bar。由于读取一个volatile字段需要所有内存同步,这个问题通过上面的模式解决了。

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

没有。它必须保留语言规范的要求。在这种情况下,JMM。如果某些转换在 JMM 下是合法的,那么就可以执行它。 "One-thread's point of view"不是规范语言,规范是。

和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.

也就是说,您可以轻松地转换程序以公开路径而无需干预写入,并且微不足道的读取重新排序会给出 "counter-intuitive" 结果。字节码不是唯一 "transformation" 该程序可以使用的。上面的 link 概述了编译器转换, 公开代码路径而没有数据依赖性 存储。 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;

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

这是一个可能出错的人为示例。在实际程序中,控制流和数据流要复杂得多,并且允许优化转换(并且最终会)产生类似的结果,因为它们在经过一公吨的计算后对一组派生规则进行操作,例如 "no synchronization, no data dependencies -- free to move stuff around"揭示有趣行为的转换。

数据竞赛是邪恶的。

有一种简单的方法可以避开大部分问题:

public enum SafeSingleton {
  INSTANCE;
}