这里:
当对象的构造函数完成时,该对象被视为已完全初始化。只有在对象完全初始化后才能看到对该对象的引用的线程保证看到该对象的最终字段的正确初始化值。
volatile
领域是否提供相同的保证? 如果以下示例中的y
字段是volatile
我们可以观察到0
怎么办?
class FinalFieldExample {
final int x;
int y;
static FinalFieldExample f;
public FinalFieldExample() {
x = 3;
y = 4;
}
static void writer() {
f = new FinalFieldExample();
}
static void reader() {
if (f != null) {
int i = f.x; // guaranteed to see 3
int j = f.y; // could see 0
}
}
}
是的,可以看到0
class FinalFieldExample {
final int x;
volatile int y;
static FinalFieldExample f;
...
}
简短的解释:
writer()
线程通过数据争用发布f = new FinalFieldExample()
对象- 由于这种数据争用,
reader()
线程被允许f = new FinalFieldExample()
对象视为半初始化.
特别是,reader()
线程可以看到y = 4;
之前的值y
- 即初始值0
。
更详细的解释在这里。
您可以使用此jcstress测试在ARM64上重现此行为。
我认为阅读 0 是可能的。
规范说:
对易失性变量的写入
v
与任何线程对v
的所有后续读取同步(其中"后续"是根据同步顺序定义的)。
在我们的例子中,我们有同一个变量的写入和读取,但没有任何东西可以确保读取是后续的。特别是,写入和读取发生在与任何其他同步操作无关的不同线程中。
也就是说,读取可能会在按同步顺序写入之前进行。
这听起来可能令人惊讶,因为写入线程在y
后写入f
,而读取线程仅在检测到已写入f
时才读取y
。但由于写入和读取到f
不同步,因此以下引用适用:
更具体地说,如果两个操作共享发生前关系,则对于不与它们共享发生前关系的任何代码,它们不一定必须按该顺序显示已发生。例如,一个线程中的写入与另一个线程中的读取处于数据争用状态,这些读取可能看起来不按顺序发生。
示例 17.4.1 的解释性注释还重申允许运行时对这些写入重新排序:
如果某个执行表现出这种行为,那么我们就会知道指令 4 在指令 1 之前,指令 2 在指令 2 之前,在指令 3 之前,在指令 4 之前。从表面上看,这是荒谬的。
但是,允许编译器对任一线程中的指令重新排序,前提是这不会影响该线程的单独执行。
在我们的例子中,单独地,写入线程的行为不受对写入进行重新排序的影响f
和y
.
是的,当x
易失性时,0
是可能的,因为不能保证writer()
线程中的写入x = 3
总是发生在reader()
线程中的读取local_f.x
之前。
class FinalFieldExample {
volatile int x;
static FinalFieldExample f;
public FinalFieldExample() {
x = 3;
}
static void writer() {
f = new FinalFieldExample();
}
static void reader() {
var local_f = f;
if (local_f != null) {
int i = local_f.x; // could see 0
}
}
}
因此,即使x
volatile
(这意味着所有读取和写入x
都按全局顺序进行),也没有什么能阻止reader()
线程中的读取local_f.x
发生在writer()
线程中的写入x = 3
之前<。 在这些情况下,br/>local_f.x
将返回0
(int
的默认值, 其工作方式类似于初始写入)。
问题是,在reader()
线程读取f
之后,不能保证(即没有发生之前的关系)它正确地看到f
的内部状态:即它可能看不到writer()
线程在构造函数中f.x
FinalFieldExample
写入x = 3
。
您可以通过以下方式创建此发生前关系:
- 要么制造
f
volatile
(x
可以制成非易失性)
来自 JLS:class FinalFieldExample { int x; static volatile FinalFieldExample f; ... }
对易失性字段 (§8.3.1.4) 的写入发生在每次后续读取该字段之前。
- 或制作
x
final
而不是volatile
来自 JLS:class FinalFieldExample { final int x; static FinalFieldExample f; ... }
当对象的构造函数完成时,该对象被视为已完全初始化。只有在对象完全初始化后才能看到对该对象的引用的线程保证看到该对象的最终字段的正确初始化值。
编辑:我下面的答案看起来是错误的。volatile
只要求在进行写入时完成所有读取和写入(以及其他"操作"),但后续写入仍然可以重新排序,以便在写入volatile
之前发生。 因此,可以在写入y
之前查看f
。
这真的很奇怪,但我们来了。
user17206833 在我上面的答案似乎是正确的,并且包含一个非常有用资源的链接,我建议您查看一下。
错误的东西(我把它留在那里,因为它说明了一个常见的误解):
OP 我想我误读了你的问题:
"如果以下示例中的 y 字段是易失性的,我们可以观察到 0 吗?">
如果y
是易失性的,那么不,你不能观察到0。
class FinalFieldExample {
final int x;
volatile int y;
如果这是您的意思,那么写入y
后再读取y
必须为读取创建一个先发生前边。 JLS 说:"对易失性字段 (§8.3.1.4) 的写入发生在该字段的每次后续读取之前">,并且从不限定需要读取某种类型的引用的语句。f
既不volatile
也不final
这一事实应该没有什么区别。
首先,volatile
和初始化是不相关的概念:字段的初始化保证不受其volatile
的影响。
除非this
从构造函数内部"转义"(这里不是这种情况),否则构造函数保证在任何其他进程可以访问实例的字段/方法之前完成执行,因此y
必须在reader()
中初始化,如果f != null
,即
int j = f.y; // will always see 4
见JLSvolatile