你能解释一下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
}
}
}发布于 2018-07-18 17:58:25
你能解释一下
f.y的值是0而不是4吗?
在Java中,编译器/JVM执行的重要优化之一是重新排序指令。只要它不违反语言规范,出于效率原因,编译器就可以自由地重新排序所有指令。在对象构造期间,可以实例化对象、完成构造函数以及在正确初始化对象中的所有字段之前发布其引用。
但是,Java说,如果一个字段被标记为final,那么必须在构造函数完成时正确地初始化它。引用来自您所引用的Java语言规范部分的内容。重点是我的。
当构造函数完成时,对象被认为是完全初始化的。只有在对象被完全初始化后才能看到对对象的引用的线程,保证看到该对象的最终字段的值正确初始化。
因此,当FinalFieldExample被构造并分配给f时,x字段必须被正确初始化为3,但是y字段可能已经被正确初始化,也可能没有被正确初始化。因此,如果thread1调用writer(),然后thread2调用reader(),并将f视为null,则y可能为0(尚未初始化)或4(初始化)。
发布于 2018-07-16 16:34:49
假设我们已经启动了两个线程,如下所示:
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在Thread #1编写操作之前读取f.y。
还请注意,对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规范的更多技术部分也包含类似的措辞。
https://stackoverflow.com/questions/51365246
复制相似问题