您能解释一下如何将f.y的值看成0而不是4吗? 那是因为其他线程写入将值从4更新为0? 此示例摘自jls https://docs.oracle.com/javase/specs/jls/se8/html/jls-17.html#jls-17.5
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 :(得分:4)
假设我们启动了两个线程,如下所示:
new Thread(FinalFieldExample::writer).start(); // Thread #1
new Thread(FinalFieldExample::reader).start(); // Thread #2
我们可能会观察到程序的实际操作顺序如下:
Thread #1
写入x = 3
。Thread #1
写入f = ...
。Thread #2
读取f
并发现它不是null
。Thread #2
读取f.x
并看到3
。Thread #2
读取f.y
并看到0
,因为似乎尚未写入y
。Thread #1
写入y = 4
。换句话说,Threads #1
和#2
能够以Thread #2
在f.y
写入之前先读取Thread #1
的方式进行操作交织。
还要注意,允许对static
字段f
的写操作进行重新排序,以便它似乎在写f.y
之前发生。这只是缺少任何类型的同步的另一个结果。如果我们也将f
也声明为volatile
,则将避免这种重新排序。
评论中有一些关于通过反射写入final
字段的说法,这是事实。 §17.5.3中对此进行了讨论:
在某些情况下,例如反序列化,系统将需要在构造后更改对象的
final
字段。final
字段可以通过反射和其他依赖实现的方式进行更改。
因此,在一般情况下,Thread #2
在读取f.x
时可以看到任何值。
还有一种更常规的方式来查看final
字段的默认值,方法是在分配之前简单地泄漏this
:
class Example {
final int x;
Example() {
leak(this);
x = 5;
}
static void leak(Example e) { System.out.println(e.x); }
public static void main(String[] args) { new Example(); }
}
我认为,如果FinalFieldExample
的构造函数是这样的:
static FinalFieldExample f;
public FinalFieldExample() {
f = this;
x = 3;
y = 4;
}
Thread #2
也可以将f.x
读为0
。
这来自§17.5:
当对象的构造函数完成时,该对象被视为完全初始化。保证只有在对象完全初始化后才能看到对对象的引用,该线程才能看到该对象的
final
字段的正确初始化值。
final
的规范中技术性更高的部分也包含类似的措辞。
答案 1 :(得分:1)
您能解释一下如何将
f.y
的值看成0而不是4吗?
在Java中,编译器/ JVM执行的重要优化之一是指令的重新排序。只要不违反语言规范,出于效率考虑,编译器可以自由地对所有指令重新排序。在对象构造期间,可以实例化对象,构造函数可以完成,并且其引用 before 中已正确初始化了对象中的所有字段。
但是,Java语言指出,如果将字段标记为final
,则必须在构造函数完成时对其进行正确的初始化。引用Java language specs you reference部分。重点是我的。
当对象的构造函数完成时,就认为该对象已完全初始化。保证只有在对象完全初始化之后才能看到对对象的引用的线程才能保证看到该对象的最终字段的正确初始化的值。
因此,在构造FinalFieldExample
并将其分配给f
时,x
字段必须正确初始化为3,但是{{1} }字段可能已正确初始化,也可能未正确初始化。因此,如果线程1调用了y
,然后线程2调用了writer()
,并且将reader()
视为不为空,则f
可以为0(尚未初始化)或4 (已初始化)。