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.
但是常规的、好的旧的非 volatile 字段呢?我见过很多多线程代码,在使用非 volatile 字段构造对象后,它们不会费心创建任何类型的内存屏障。但我从未见过或听说过任何问题,而且我自己也无法重新创建这样的部分构造。
现代 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
执行了我们感兴趣的两次写入:
- T1 将值 5 写入新实例化的
myFoo
的bar
- T1 将对新创建对象的引用写入
myFoo
变量
对于 T1,我们得到一个 guarantee写#1 发生在之前写#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
最佳答案
以您的示例作为问题本身 - 答案是是,这是完全可能的。正如您引用的那样,初始化字段仅对构造线程可见。这称为安全发布(但我打赌你已经知道了)。
事实是您没有通过实验看到这一点是 x86 上的 AFAIK(作为一个强大的内存模型),商店不会重新排序,所以除非 JIT 会重新排序这些商店T1 做到了 - 你看不到。但那是在玩火,字面意思,this question和后续行动(接近相同)here一个人(不确定是否属实)丢失了 1200 万台设备
JLS 只保证了几种实现可见性的方法。顺便说一句,这不是相反,JLS 不会说什么时候会中断,它会说什么时候会工作。
请注意该示例如何显示每个 字段必须是final
- 即使在当前实现下一个单个 就足够了,并且在构造函数之后插入了两个内存屏障(当使用 final 时):LoadStore
和 StoreStore
。
2) volatile fields (并且隐含地 AtomicXXX
);我认为这个不需要任何解释,而且你似乎引用了这个。
3) Static initializers好吧,在我看来应该是显而易见的
4) Some locking involved - 这也应该是显而易见的,发生在规则之前...
关于java - 对象构造在实践中是否保证所有线程都看到已初始化的非最终字段?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/51695962/