【发布时间】:2026-01-07 21:55:01
【问题描述】:
Java memory model 保证对象的构造和终结器之间的先发生关系:
从一个构造函数的末尾有一个happens-before边 对象指向该对象的终结器(第 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 执行了两个我们感兴趣的写入操作:
- T1 将值 5 写入新实例化的
myFoo的bar - T1 将对新创建的对象的引用写入
myFoo变量
对于 T1,我们得到一个 guarantee 写 #1 happened-before 写 #2:
线程中的每个动作在该线程中的每个动作之前发生 它在程序的顺序中稍后出现。
但就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
【问题讨论】:
-
我没有注意到任何栅栏或
lock前缀指令,无论是在一个类仅包含一个最终字段的情况下,还是在一个类仅包含一个非最终字段的情况下。让我看起来有点困惑...... -
究竟是什么阻止了最终字段初始化的重新排序?
-
他们对
final字段的保证安全性非常明确。如果相同的规则适用于 all 字段的构造,我认为没有必要。关于难以重现的并发问题,请不要屏住呼吸。正如this question 所表达的那样,即使是记录在案的问题似乎也无法重现。 -
@VinceEmigh 我不认为那篇文档的作者对我们含糊其辞。显然,不能保证常规字段。但轶事证据表明它在实践中不会发生。因此,也许对 JVM 更了解的人可能会对此有所了解。毕竟,这不是吹毛求疵。如果这样的对象构造在实践中实际上是不安全的,则必须修复大量代码。
-
@Malt 这不是要害羞,而是他们不需要添加特定声明以使 JLS 保持有效。看看我的回答。让我知道您是否希望我从规范中编译一些参考资料,这些参考资料解释了应该预期的行为。它不是特定于此,但是此的行为在技术上已记录在案。
标签: java multithreading jvm java-memory-model