我试着理解最终字段的语义。
让研究代码:
public class App {
final int[] data;
static App instance;
public App() {
this.data = new int[]{1, 0};
this.data[1] = 2;
}
public static void main(String[] args) {
new Thread(new Runnable() {
public void run() {
instance = new App();
}
}).start();
while (instance == null) {/*NOP*/}
System.out.println(Arrays.toString(instance.data));
}
}
我有一些问题:
P.S。我不知道如何使标题正确,随时可以编辑。
如果我们更换,是否存在可见性差异:
public App() {
this.data = new int[]{1, 0};
this.data[1] = 2;
}
与
public App() {
int [] data = new int[]{1, 0};
data[1] = 2;
this.data = data;
}
我也想知道wjat将在我的例子中用volatile替换final
。
因此,我希望得到关于4个新案例的解释
答案 0 :(得分:6)
是的,如果应用程序终止,它将输出[1,2]
。关键是final
字段语义作为一个整体应用于构造函数,将数组引用写入字段时的确切时间是无关紧要的。这也意味着在构造函数中,重新排序是可能的,因此如果this
引用在构造函数完成之前转义,则所有保证都是无效的,无论this
是否在程序中的写入之前或之后转义订购。因为在你的代码中,this
在构造函数完成之前没有转义,所以保证适用。
请参阅JLS §17.5., final
Field Semantics:
当构造函数完成时,对象被认为是完全初始化。在该对象完全初始化之后只能看到对象引用的线程可以保证看到该对象的
final
字段的正确初始化值。
请注意,它指的是完全初始化的状态,而不是对特定final
字段的写入。这也将在下一节§17.5.1中解决:
让 o 成为对象, c 是 o 的构造函数,其中
final
字段 f 写的。当 c 正常或突然退出时,会对 o 的final
字段 f 执行冻结操作。
如果您将变量更改为volatile
,则几乎不会有任何保证。 volatile
字段在对该变量的写入与后续读取之间建立发生之前关系,但经常被忽略的关键点是“后续” 。如果App
实例未正确发布,就像在您的示例中一样,则不保证主线程对instance.data
的读取将是后续的。如果它读取现在可能的null
引用,那么您知道它不是后续的。如果它读取非null
引用,您知道它是在字段写入之后,这意味着您可以保证在第一个插槽中读取1
,但是对于第二个插槽,您可能会读到0
或2
。
如果您想根据障碍和重新排序来讨论这个问题,volatile
写入data
会保证所有先前的写入都已提交,其中包括将1
写入第一个数组槽,但不保证后续的非volatile
写入不会提前提交。因此,App
引用的不正确发布仍然可能在volatile
写入之前执行(尽管很少发生)。
如果将写入移动到构造函数的末尾,则在看到非null
数组引用后,所有先前的写入都是可见的。对于final
字段,它不需要进一步讨论,如上所述,写入在构造函数中的实际位置无论如何都是无关紧要的。对于volatile
情况,如上所述,您不能保证读取非null
引用,但是当您读取它时,所有先前的写入都将被提交。知道表达式new int[]{1, 0};
无论如何都被编译为等效的hiddenVariable=new int[2]; hiddenVariable[0]=1; hiddenVariable[1]=0;
可能会有所帮助。在构造之后但在volatile
写入对字段的数组引用之前放置另一个数组,不会改变语义。