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

Mal*_*alt 9 java multithreading jvm java-memory-model

Java的内存模型保证了之前发生的对象的构造和释放的关系:

从对象的构造函数的末尾到该对象的终结器(第12.6节)的开始有一个发生前的边缘.

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

当构造函数完成时,对象被认为是完全初始化的.在该对象完全初始化之后只能看到对象引用的线程可以保证看到该对象的最终字段的正确初始化值.

还有一个关于volatile字段的保证,因为关于对这些字段的所有访问的关系都存在:

写入易失性字段(第8.3.1.4节) - 在每次后续读取该字段之前发生.

但是常规的,古老的非易失性领域呢?我已经看到很多多线程代码在使用非易失性字段构造对象后不会创建任何类型的内存屏障.但是我从来没有见过或听说过任何问题,而且我自己也无法重建这种局部结构.

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

编辑:

澄清一下,我说的是以下情况.假设我们有一个班级:

public class Foo{
    public int bar = 0;

    public Foo(){
        this.bar = 5;
    }
    ...
}
Run Code Online (Sandbox Code Playgroud)

一些Thread T1实例化一个新Foo实例:

Foo myFoo = new Foo();

然后将实例传递给其他线程,我们将调用它T2:

Thread t = new Thread(() -> {
     if (myFoo.bar == 5){
         ....
     }
});
t.start();
Run Code Online (Sandbox Code Playgroud)

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

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

对于T1,我们得到写#1 发生保证- 在写#2 之前:

线程中的每个动作都发生在该线程中的每个动作之前,该动作在程序的顺序中稍后出现.

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

EDIT2:

我写了几个月后再看了上面的例子.并且该代码实际上保证T2T1写入之后开始正常工作.对于我想问的问题,这是一个不正确的例子.修复它假设T2在T1执行写操作时已经在运行.Say T2myFoo在循环中读取,如下所示:

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
Run Code Online (Sandbox Code Playgroud)

Eug*_*ene 3

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

事实上,您没有通过实验看到这一点,据我所知,在 x86(作为强大的内存模型)上,存储无论如何都不会重新排序,因此除非 JIT 重新排序 T1 所做的那些存储 - 您看不到这一点。但这是在玩火,字面意思是,这个问题和后续问题(几乎是一样的),一个人(不确定是否属实)丢失了1200 万装备

JLS 仅保证通过几种方式实现可见性。顺便说一句,这不是相反的情况,JLS 不会说它什么时候会坏,它会说它什么时候会起作用

1) 最终字段语义

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

2) 易失性字段(并且隐式地AtomicXXX);我认为这个不需要任何解释,看来你引用了这一点。

3)静态初始化器很好,在我看来应该是显而易见的

4)涉及一些锁定- 这也应该是显而易见的,发生在规则之前......