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

时间:2018-08-05 16:03:14

标签: java multithreading jvm java-memory-model

Java memory model保证了对象的构造和终结器之间发生先于关系:

  

从   对象指向该对象的终结器(§12.6)的开头。

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

  

当一个对象被认为是完全初始化的   构造函数完成。只能看到引用的线程   该对象已完全初始化后的对象   查看该对象最终的正确初始化的值   字段。

对于volatile字段也有保证,因为与对这些字段的所有访问都存在事前关联:

  

在随后的每个后续操作之前,都会对volatile字段(第8.3.1.4节)进行写操作   阅读该字段。

但是常规的,良好的旧非易失性字段呢?我已经看到了很多多线程代码,这些对象在使用非易失性字段构造对象之后不会麻烦创建任何类型的内存屏障。但是由于这个原因,我从未见过或听到任何问题,而且我自己也无法重建这种局部构造。

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

编辑:

为澄清起见,我正在谈论以下情况。说我们有一堂课:

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写入新实例化的bar的{​​{1}}
  2. T1将对新创建对象的引用写到myFoo变量

对于T1,我们得到一个guarantee,它写#1 <发生前发生了写#2:

  

线程中的每个动作发生在该线程之前   会按照程序的顺序出现。

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

编辑2:

写完几个月后,我再次看了上面的示例。自从bar写完之后开始T2以来,该代码实际上可以保证正常工作。这使其成为我要提出的问题的错误示例。修复了假设T1执行写操作时T2已在运行的问题。假设T1正在循环读取T2,如下所示:

myFoo

3 个答案:

答案 0 :(得分:3)

  

但是轶事证据表明它实际上并没有发生

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

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

答案 1 :(得分:3)

似乎在对象构建期间没有没有同步

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?

答案 2 :(得分:3)

以您的示例作为问题本身-答案将是,这完全有可能。就像您引用的那样,初始化的字段仅对构造线程可见( )。这就是安全出版物(但我敢打赌,您已经对此有所了解)。

您没有通过实验看到的事实是x86上的AFAIK(是强大的内存模型),存储不会重新排序,因此除非{{1 }}将重新排序JIT所做的商店-您看不到。但这真是令人发指,this question和一个后续行动(几乎是相同的)here的一个人(不确定是否正确)损失了 1200万设备< / em>

T1仅保证获得可见性的几种方法。而且,绕过btw并不是另一回事,JLS不会说这将何时中断,而会说何时将将起作用

1) final field semantics

请注意示例如何显示每个字段必须为JLS-即使在当前实现中,一个单个就足够了,并且有两个在构造函数finalLoadStore之后插入(当使用final时)内存屏障。

2) volatile fields(并隐含StoreStore);我认为这个不需要任何解释,似乎您引用了这个。

3) Static initializers好吧,应该是显而易见的IMO

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