我刚跑过an article这提出了我以前从未听说过并且在其他任何地方都找不到的说法。声称从另一个线程的角度来看,构造函数返回的值的分配可能会根据构造函数内部的指令重新排序。换句话说,声称在下面的代码中,另一个线程可以读取非空值 a
其中x
的值尚未设置。
class MyInt {
private int x;
public MyInt(int value) {
x = value;
}
public int getValue() {
return x;
}
}
MyInt a = new MyInt(42);
这是真的吗?
编辑:
我认为从执行
MyInt a = new MyInt(42)
的线程的角度来看,这是有保证的。 ,分配x
与 a
的赋值有一个发生在之前的关系.但是这两个值都可能缓存在寄存器中,并且它们可能不会按照最初写入的顺序刷新到主内存中。没有内存屏障,另一个线程因此可以读取 a
的值。在 x
的值之前已写。正确的?所以基于 axtavt的回答和随后的评论,这些对线程安全的评估是否正确?
// thread-safe
class Foo() {
final int[] x;
public Foo() {
int[] tmp = new int[1];
tmp[0] = 42;
x = tmp; // memory barrier here
}
}
// not thread-safe
class Bar() {
final int[] x = new int[1]; // memory barrier here
public Bar() {
x[0] = 42; // assignment may not be seen by other threads
}
}
如果那是正确的......哇,那真的很微妙。
最佳答案
您引用的文章在概念上是正确的。它的术语和用法有点不精确,就像你的问题一样,这会导致潜在的误解和误解。似乎我在这里对术语喋喋不休,但 Java 内存模型非常微妙,如果术语不准确,那么人们的理解就会受到影响。
我将从您的问题(和评论)中摘录要点并提供答复。
The assignment of the value returned by a constructor may be reordered with respect to instructions inside the constructor.
几乎是的......它不是指令,而是可以重新排序的内存操作(读取和写入)。一个线程可以按特定顺序执行两条写入指令,但数据到达内存,以及这些写入对其他线程的可见性,可能以不同的顺序发生。
I think it's guaranteed that from the perspective of the thread executing
MyInt a = new MyInt(42)
, the assignment ofx
has a happens-before relationship with the assignment ofa
.
再次,几乎。确实,在程序顺序中是对
x
的赋值。在分配给 a
之前发生.但是,happens-before 是一个适用于所有线程的全局属性,因此针对特定线程谈论happens-before 是没有意义的。But both of these values may be cached in registers, and they may not be flushed to main memory in the same order they were originally written. Without a memory barrier, another thread could therefore read the value of a before the value of x has been written.
再一次,几乎。值可以缓存在寄存器中,但部分内存硬件(如缓存内存或写入缓冲区)也可能导致重新排序。硬件可以使用多种机制来更改排序,例如缓存刷新或内存屏障(通常不会导致刷新,而只是阻止某些重新排序)。然而,从硬件的角度考虑这一点的困难在于,实际系统非常复杂并且具有不同的行为。例如,大多数 CPU 有几种不同风格的内存屏障。如果您想对 JMM 进行推理,您应该考虑模型的元素:内存操作和同步,它们通过建立发生在之前的关系来限制重新排序。
因此,要根据 JMM 重新审视这个示例,我们会看到对字段
x
的写入。和写入字段 a
按程序顺序。该程序中没有任何内容限制重新排序,即没有同步,没有对 volatile 的操作,没有写入最终字段。这些写入之间没有发生之前的关系,因此它们可以重新排序。有几种方法可以防止这些重新排序。
一种方法是制作
x
最后。这是有效的,因为 JMM 说在构造函数返回之前写入 final 字段,在构造函数返回之后发生的操作之前发生。自 a
写在构造函数返回后,初始化final字段x
在写入 a
之前发生,并且不允许重新排序。另一种方法是使用同步。假设
MyInt
实例在另一个类中使用,如下所示:class OtherObj {
MyInt a;
synchronized void set() {
a = new MyInt(42);
}
synchronized int get() {
return (a != null) ? a.getValue() : -1;
}
}
结尾的解锁
set()
调用发生在写入 x
之后和 a
领域。如果另一个线程调用 get()
,它在调用开始时需要一个锁。这在 set()
末尾的锁释放之间建立了一个发生在之前的关系。以及get()
开头的锁获取.这意味着写入 x
和 a
不能在 get()
开头后重新排序打电话。因此,阅读器线程将看到 a
的有效值。和 x
并且永远找不到非空的 a
和一个未初始化的 x
.当然如果读者线程调用
get()
早些时候,它可能会看到 a
为空,但这里没有内存模型问题。您的
Foo
和 Bar
例子很有趣,你的评估基本上是正确的。在分配给最终数组字段之前发生的对数组元素的写入不能在之后重新排序。在分配给最终数组字段之后发生的对数组元素的写入可能会相对于稍后发生的其他内存操作重新排序,因此其他线程可能确实会看到过时的值。在您询问这是否是
String
的问题的评论中因为它有一个包含其字符的最终字段数组。是的,这是一个问题,但是如果您查看 String.java 构造函数,它们都非常小心地对构造函数末尾的最终字段进行赋值。这确保了数组内容的正确可见性。是的,这很微妙。 :-) 但问题只有在您尝试变得聪明时才会真正发生,例如尝试避免使用同步或 volatile 变量。大多数时候这样做是不值得的。如果您坚持“安全发表”的做法,包括不泄露
this
在构造函数调用期间,并使用同步存储对构造对象的引用(例如我的 OtherObj
上面的示例),事情将完全按照您的预期工作。引用资料:
关于java - 构造函数和指令重新排序,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/24789287/