对象构造在实践中是否保证所有线程都看到已初始化的非最终字段?

Does object construction guarantee in practice that all threads see non-final fields initialized?

Java memory model 保证对象的构造和终结器之间存在先行关系:

There is a happens-before edge from the end of a constructor of an object to the start of a finalizer (§12.6) for that object.

以及final字段的构造函数和初始化:

An object is considered to be completely initialized when its constructor finishes. A thread that can only see a reference to an object after that object has been completely initialized is guaranteed to see the correctly initialized values for that object's final fields.

还有关于 volatile 字段的保证,因为对于所有对此类字段的访问存在先行关系:

A write to a volatile field (§8.3.1.4) happens-before every subsequent read of that field.

但是常规的、好的旧的非易失性字段呢?我见过很多多线程代码,在使用非易失性字段构造对象后,它们不会费心创建任何类型的内存屏障。但我从未见过或听说过任何问题,而且我自己也无法重新创建这样的部分构造。

现代 JVM 是否只是在构建之后设置内存屏障?避免在施工期间重新排序?还是我只是幸运?如果是后者,是否可以随意编写重现部分构造的代码?

编辑:

澄清一下,我说的是以下情况。假设我们有一个 class:

public class Foo{
    public int bar = 0;

    public Foo(){
        this.bar = 5;
    }
    ...
}

并且一些线程 T1 实例化了一个新的 Foo 实例:

Foo myFoo = new Foo();

然后将实例传递给其他线程,我们称之为 T2:

Thread t = new Thread(() -> {
     if (myFoo.bar == 5){
         ....
     }
});
t.start();

T1 进行了两次我们感兴趣的写入:

  1. T1 将值 5 写入新实例化的 myFoo
  2. bar
  3. T1 将对新创建对象的引用写入 myFoo 变量

对于 T1,我们得到一个 guarantee 写 #1 happened-before 写 #2:

Each action in a thread happens-before every action in that thread that comes later in the program's order.

但就 T2 而言,Java 内存模型不提供此类保证。没有什么能阻止它以相反的顺序看到写入。所以它可以看到一个完全构建的 Foo 对象,但是 bar 字段等于 0.

编辑2:

写完上面的例子几个月后,我又看了一遍。由于 T2 是在 T1 写入之后启动的,因此该代码实际上可以保证正常工作。这使它成为我想问的问题的错误示例。修复它以假设 T1 执行写入时 T2 已经是 运行。假设 T2 正在循环读取 myFoo,就像这样:

Foo myFoo = null;
Thread t2 = new Thread(() -> {
     for (;;) {
         if (myFoo != null && myFoo.bar == 5){
             ...
         }
         ...
     }
});
t2.start();
myFoo = new Foo(); //The creation of Foo happens after t2 is already running

But anecdotal evidence suggests that it doesn't happen in practice

要看到这个问题,您必须避免使用任何内存屏障。例如如果您使用任何类型的线程安全集合或一些 System.out.println 可以防止问题发生。

虽然我刚刚为 x64 上的 Java 8 update 161 编写的简单测试没有显示此问题,但我之前已经看到了这个问题。

似乎对象构造期间没有同步

JLS 不允许这样做,我也无法在代码中产生任何迹象。但是,有可能产生反对意见。

运行以下代码:

public class Main {
    public static void main(String[] args) throws Exception {
        new Thread(() -> {
            while(true) {
                new Demo(1, 2);
            }
        }).start(); 
    }
}

class Demo {
    int d1, d2;

    Demo(int d1, int d2) {
        this.d1 = d1;   

        new Thread(() -> System.out.println(Demo.this.d1+" "+Demo.this.d2)).start();

        try {
            Thread.sleep(500);
        } catch(InterruptedException e) {
            e.printStackTrace();
        }

        this.d2 = d2;   
    }
}

输出将持续显示1 0,证明创建的线程能够访问部分创建的对象的数据。

但是,如果我们同步这个:

Demo(int d1, int d2) {
    synchronized(Demo.class) {
        this.d1 = d1;   

        new Thread(() -> {
            synchronized(Demo.class) {
                System.out.println(Demo.this.d1+" "+Demo.this.d2);
            }
        }).start();

        try {
            Thread.sleep(500);
        } catch(InterruptedException e) {
            e.printStackTrace();
        }

        this.d2 = d2;   
    }
}

输出是 1 2,表明新创建的线程实际上会等待锁,这与未同步的例子相反。

相关:Why can't constructors be synchronized?

以你的例子作为问题本身 - 答案是,这是完全可能的。正如您引用的那样,初始化字段对构造线程可见。这称为 安全发布(但我打赌你已经知道了)。

事实上你没有通过实验看到这一点是 x86 上的 AFAIK(是一个强大的内存模型),商店 不是 重新排序,所以除非 JIT 会重新订购 T1 做过的那些商店 - 你看不到。但那是在玩火,从字面上看,this question and the follow-up (it's close to the same) here 一个人(不确定是否属实)丢失了 1200 万设备

JLS 只保证几种实现可见性的方法。顺便说一句,这不是相反,JLS 不会说什么时候会崩溃,它会说什么时候 会工作 .

1) final field semantics

请注意该示例如何显示 每个 字段必须是 final - 即使在当前实现下 单个 就足够了,并且在构造函数之后插入了两个内存屏障(当使用 final(s) 时):LoadStoreStoreStore.

2) volatile fields(隐含地 AtomicXXX);我认为这个不需要任何解释,而且你似乎引用了这个。

3) Static initializers 嗯,在我看来应该是显而易见的

4) Some locking involved - 这也应该很明显,发生在规则之前...