对象构造在实践中是否保证所有线程都看到已初始化的非最终字段?
Does object construction guarantee in practice that all threads see non-final fields initialized?
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.
但是常规的、好的旧的非易失性字段呢?我见过很多多线程代码,在使用非易失性字段构造对象后,它们不会费心创建任何类型的内存屏障。但我从未见过或听说过任何问题,而且我自己也无法重新创建这样的部分构造。
现代 JVM 是否只是在构建之后设置内存屏障?避免在施工期间重新排序?还是我只是幸运?如果是后者,是否可以随意编写重现部分构造的代码?
编辑:
澄清一下,我说的是以下情况。假设我们有一个 class:
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:
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
But anecdotal evidence suggests that it doesn't happen in practice
要看到这个问题,您必须避免使用任何内存屏障。例如如果您使用任何类型的线程安全集合或一些 System.out.println
可以防止问题发生。
虽然我刚刚为 x64 上的 Java 8 update 161 编写的简单测试没有显示此问题,但我之前已经看到了这个问题。
似乎对象构造期间没有同步。
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?
以你的例子作为问题本身 - 答案是是,这是完全可能的。正如您引用的那样,初始化字段对构造线程可见仅。这称为 安全发布(但我打赌你已经知道了)。
事实上你没有通过实验看到这一点是 x86 上的 AFAIK(是一个强大的内存模型),商店 不是 重新排序,所以除非 JIT 会重新订购 T1 做过的那些商店 - 你看不到。但那是在玩火,从字面上看,this question and the follow-up (it's close to the same) here 一个人(不确定是否属实)丢失了 1200 万设备
JLS 只保证几种实现可见性的方法。顺便说一句,这不是相反,JLS 不会说什么时候会崩溃,它会说什么时候 会工作 .
请注意该示例如何显示 每个 字段必须是 final
- 即使在当前实现下 单个 就足够了,并且在构造函数之后插入了两个内存屏障(当使用 final(s) 时):LoadStore
和 StoreStore
.
2) volatile fields(隐含地 AtomicXXX
);我认为这个不需要任何解释,而且你似乎引用了这个。
3) Static initializers 嗯,在我看来应该是显而易见的
4) Some locking involved - 这也应该很明显,发生在规则之前...
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.
但是常规的、好的旧的非易失性字段呢?我见过很多多线程代码,在使用非易失性字段构造对象后,它们不会费心创建任何类型的内存屏障。但我从未见过或听说过任何问题,而且我自己也无法重新创建这样的部分构造。
现代 JVM 是否只是在构建之后设置内存屏障?避免在施工期间重新排序?还是我只是幸运?如果是后者,是否可以随意编写重现部分构造的代码?
编辑:
澄清一下,我说的是以下情况。假设我们有一个 class:
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
的 - T1 将对新创建对象的引用写入
myFoo
变量
bar
对于 T1,我们得到一个 guarantee 写 #1 happened-before 写 #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
But anecdotal evidence suggests that it doesn't happen in practice
要看到这个问题,您必须避免使用任何内存屏障。例如如果您使用任何类型的线程安全集合或一些 System.out.println
可以防止问题发生。
虽然我刚刚为 x64 上的 Java 8 update 161 编写的简单测试没有显示此问题,但我之前已经看到了这个问题。
似乎对象构造期间没有同步。
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?
以你的例子作为问题本身 - 答案是是,这是完全可能的。正如您引用的那样,初始化字段对构造线程可见仅。这称为 安全发布(但我打赌你已经知道了)。
事实上你没有通过实验看到这一点是 x86 上的 AFAIK(是一个强大的内存模型),商店 不是 重新排序,所以除非 JIT 会重新订购 T1 做过的那些商店 - 你看不到。但那是在玩火,从字面上看,this question and the follow-up (it's close to the same) here 一个人(不确定是否属实)丢失了 1200 万设备
JLS 只保证几种实现可见性的方法。顺便说一句,这不是相反,JLS 不会说什么时候会崩溃,它会说什么时候 会工作 .
请注意该示例如何显示 每个 字段必须是 final
- 即使在当前实现下 单个 就足够了,并且在构造函数之后插入了两个内存屏障(当使用 final(s) 时):LoadStore
和 StoreStore
.
2) volatile fields(隐含地 AtomicXXX
);我认为这个不需要任何解释,而且你似乎引用了这个。
3) Static initializers 嗯,在我看来应该是显而易见的
4) Some locking involved - 这也应该很明显,发生在规则之前...