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

问题描述 投票:9回答:3

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

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

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

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

还有一个关于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写入新实例化的barmyFoo
  2. T1将对新创建的对象的引用写入myFoo变量

对于T1,我们得到一个写#1发生的guarantee - 在写#2之前:

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

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

Aaditi:

我写了几个月后再看了上面的例子。实际上,由于T2T1撰写之后开始使用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 ,如下所示:

x86
java multithreading jvm java-memory-model
3个回答
3
投票

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

事实上,你没有通过实验看到AFAIK在JIT(作为一个强大的记忆模型),商店无论如何都没有重新订购,所以除非T1重新订购那些this question所做的商店 - 你看不到。但那是玩火,文字,here和后续(它接近相同)JLS的一个人(不确定是否真的)丢失了12百万的设备

1) final field semantics只保证了几种实现可见性的方法。而且不是btw的另一种方式,JLS不会说这何时会破坏,它会说什么时候会起作用。

final

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

AtomicXXX(和隐含的3) Static initializers);我认为这个不需要任何解释,似乎你引用了这个。

4) Some locking involved好,有点应该是明显的IMO

System.out.println - 这应该是显而易见的,发生在规则之前......


3
投票

但轶事证据表明,这在实践中并未发生

要查看此问题,您必须避免使用任何内存障碍。例如如果你使用任何类型的线程安全集合或一些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; } } 可以防止问题发生。

我之前看到过这个问题虽然我刚刚在x64上为Java 8更新161编写的简单测试没有显示这个问题。


3
投票

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

JLS不允许它,也不能在代码中产生任何迹象。但是,有可能产生反对意见。

运行以下代码:

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?,表明新创建的线程实际上将等待锁定,与未同步的exampled相反。

相关:qazxswpoi

© www.soinside.com 2019 - 2024. All rights reserved.