我目前正在阅读JSR-133(Java内存模型),我无法理解为什么f.y可能未初始化(可能看到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 :(得分:9)
这称为"过早发布"效果。
简单来说,允许JVM重新排序程序指令(出于性能原因),如果这样的重新排序没有违反JMM的限制。
您希望代码f = new FinalFieldExample();
的运行方式如下:
1。创建FinalFieldExample
的实例
2.将3分配给x
3.将4分配给y
4.将创建的对象分配给变量f
但是在提供的代码中,没有任何东西可以阻止JVM进行指令重新排序,所以可以运行代码,如:
1。创建FinalFieldExample
的实例
2.将3分配给x
3.将原始的,未完全初始化的对象分配给变量f
4.将4分配给y
如果在单线程环境中重新排序,我们甚至都不会注意到它。这是因为我们期望在我们开始使用它们之前完全创建对象,JVM尊重我们的期望。现在,如果多个线程同时运行此代码,会发生什么?在下一个示例中,Thread1正在执行方法writer()
和Thread2 - 方法reader()
:
线程1:创建FinalFieldExample
的实例
线程1:将3分配给x
线程1:将原始的,未完全初始化的对象分配给变量f
主题2:阅读f
,它不是空的
线程2:读取f.x,它是3
线程2:读取f.y,它仍为0
线程1:将4分配给y
绝对不好。为了防止JVM执行此操作,我们需要为其提供有关该程序的其他信息。对于此特定示例,有一些方法可以修复内存一致性:
y
声明为final
变量。这将导致" freeze"影响。简而言之,最终变量将在您访问它们时始终初始化,如果在构造期间没有泄露对象的引用。f
声明为volatile
变量。这将创建" synchronization order"并解决问题。简而言之,指令不能在易失性写入和易失性读取之上重新排序。分配给f
变量是易失性写入,这意味着new FinalFieldExample()
指令在分配后无法重新排序和执行。从f
变量读取是一个易失性读取,因此在它之前不能执行读取f.x
。 v-write和v-read的组合称为同步顺序,并提供所需的内存一致性。Here是一个很好的博客,可以回答您关于JMM的所有问题。
答案 1 :(得分:2)
JVM可能会重新排序内存读取和写入,因此f
的引用可能会在f.y
之前写入主内存。如果另一个线程在这两个写入之间读取f.y
,则它将读取0
。但是,如果通过写入final
或volatile
字段来创建内存屏障,则屏障之后的读取和写入无法针对屏障之前的读取和写入进行重新排序。因此,保证在另一个线程读取f
之前写入f.y
和f
。
我问了一个类似的问题here。答案更详细。
答案 2 :(得分:-1)
Java内存模型允许线程创建FinalFieldExample
,初始化final x
并在初始化非final f
之前将对FinalFieldExample实例的引用保存到y
字段。