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

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


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


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


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


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


对 volatile 字段(第 8.3.1.4 节)的写入发生在该字段的每次后续读取之前。


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


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


编辑:


为了澄清,我正在谈论以下情况。假设我们有一个类:


public class Foo{

    public int bar = 0;


    public Foo(){

        this.bar = 5;

    }

    ...

}

一些 ThreadT1实例化一个新Foo实例:


Foo myFoo = new Foo();


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


Thread t = new Thread(() -> {

     if (myFoo.bar == 5){

         ....

     }

});

t.start();

T1 执行了两个我们感兴趣的写入:


T1bar将新实例化的值 5 写入myFoo

T1 将新创建的对象的引用写入myFoo变量

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


线程中的每个操作都发生在该线程中按程序顺序稍后出现的每个操作之前。


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


编辑2:


写完几个月后,我又看了一遍上面的例子。并且该代码实际上可以保证正常工作,因为它T2是在T1's writes之后启动的。这使它成为我想问的问题的不正确示例。修复它以假设 T2 在T1执行写入时已经在运行。说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


精慕HU
浏览 123回答 3
3回答

潇湘沐

但轶事证据表明这在实践中不会发生要查看此问题,您必须避免使用任何内存屏障。例如,如果您使用任何类型的线程安全集合或某些System.out.println可以防止问题发生。我之前见过这个问题,尽管我刚刚为 x64 上的 Java 8 更新 161 编写的一个简单测试没有显示这个问题。
打开App,查看更多内容
随时随地看视频慕课网APP

相关分类

Java