JMM 保证关于 final as 字段和非 final 引用 object
JMM guarantees about final as field and non final reference to the object
我尝试理解最终字段的语义。
让我们研究代码:
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));
}
}
我有一些问题:
- jmm 是否保证 if 应用程序终止然后它输出 [1,2] ?
- jmm是否保证循环终止后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;
}
我还想知道如果在我的示例中将 final
替换为 volatile,wjat 将会是什么。
因此我想得到关于4个新病例的解释
是的,有一些收获。您是 re-reading 循环后的 instance
变量,并且由于两次读取都是活泼的,退出循环并不能保证循环后的读取读取非 null
引用。
由于这个问题不是问题的主题,假设有以下变化:
App instance;
while((instance=App.instance) == null) {/*NOP*/}
System.out.println(Arrays.toString(instance.data));
然后,如果 应用程序终止,它将输出[1,2]
。关键是 final
字段语义适用于整个构造函数,数组引用写入字段的确切时间无关紧要。这也意味着在构造函数中,重新排序是可能的,因此如果 this
引用在构造函数完成之前转义,则所有保证都是无效的,无论 this
是在程序写入之前还是之后转义命令。由于在您的代码中,this
在构造函数完成之前不会转义,因此保证适用。
参考JLS §17.5., final
Field Semantics:
An object is considered to be completely initialized when its constructor finishes. A thread that can only see a reference to an object after that object has been completely initialized is guaranteed to see the correctly initialized values for that object's final
fields.
请注意,它指的是 完全初始化 状态,而不是写入特定的 final
字段。这也在下一节中解决,§17.5.1:
Let o be an object, and c be a constructor for o in which a final
field f is written. A freeze action on final
field f of o takes place when c exits, either normally or abruptly.
如果将变量更改为 volatile
,则几乎没有任何保证。 volatile
字段在对该变量的写入和后续读取之间建立了 happens-before 关系,但经常被忽视的关键点是“随后”。如果 App
实例发布不当,就像在您的示例中一样,则不能保证主线程对 instance.data
的读取将是后续的。如果它读取 null
引用(现在可能),那么您就知道它不是后续的。如果它读取一个非 null
引用,您知道它在字段写入之后,这意味着您可以保证在第一个槽中读取 1
,但对于第二个槽,您可以读取0
或 2
.
如果您想从障碍和重新排序的角度讨论这个问题,volatile
写入 data
保证提交所有先前的写入,其中包括 1
的写入第一个数组槽,但它不保证后续的非 volatile
写入不会更早提交。因此,App
引用的不正确发布仍然有可能在 volatile
写入之前执行(尽管这种情况很少发生)。
如果将写入移动到构造函数的末尾,一旦看到非 null
数组引用,所有先前的写入都可见。对于 final
字段,不需要进一步讨论,如上所述,write 在构造函数中的实际位置无论如何都是无关紧要的。对于 volatile
的情况,如上所述,您不能保证读取非 null
引用,但是当您读取它时,所有先前的写入都会被提交。了解表达式 new int[]{1, 0};
无论如何都会被编译为 hiddenVariable=new int[2]; hiddenVariable[0]=1; hiddenVariable[1]=0;
的等价物可能会有所帮助。在构造之后但在 volatile
写入字段的数组引用之前放置另一个数组写入,不会改变语义。
我尝试理解最终字段的语义。
让我们研究代码:
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));
}
}
我有一些问题:
- jmm 是否保证 if 应用程序终止然后它输出 [1,2] ?
- jmm是否保证循环终止后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;
}
我还想知道如果在我的示例中将 final
替换为 volatile,wjat 将会是什么。
因此我想得到关于4个新病例的解释
是的,有一些收获。您是 re-reading 循环后的 instance
变量,并且由于两次读取都是活泼的,退出循环并不能保证循环后的读取读取非 null
引用。
由于这个问题不是问题的主题,假设有以下变化:
App instance;
while((instance=App.instance) == null) {/*NOP*/}
System.out.println(Arrays.toString(instance.data));
然后,如果 应用程序终止,它将输出[1,2]
。关键是 final
字段语义适用于整个构造函数,数组引用写入字段的确切时间无关紧要。这也意味着在构造函数中,重新排序是可能的,因此如果 this
引用在构造函数完成之前转义,则所有保证都是无效的,无论 this
是在程序写入之前还是之后转义命令。由于在您的代码中,this
在构造函数完成之前不会转义,因此保证适用。
参考JLS §17.5., final
Field Semantics:
An object is considered to be completely initialized when its constructor finishes. A thread that can only see a reference to an object after that object has been completely initialized is guaranteed to see the correctly initialized values for that object's
final
fields.
请注意,它指的是 完全初始化 状态,而不是写入特定的 final
字段。这也在下一节中解决,§17.5.1:
Let o be an object, and c be a constructor for o in which a
final
field f is written. A freeze action onfinal
field f of o takes place when c exits, either normally or abruptly.
如果将变量更改为 volatile
,则几乎没有任何保证。 volatile
字段在对该变量的写入和后续读取之间建立了 happens-before 关系,但经常被忽视的关键点是“随后”。如果 App
实例发布不当,就像在您的示例中一样,则不能保证主线程对 instance.data
的读取将是后续的。如果它读取 null
引用(现在可能),那么您就知道它不是后续的。如果它读取一个非 null
引用,您知道它在字段写入之后,这意味着您可以保证在第一个槽中读取 1
,但对于第二个槽,您可以读取0
或 2
.
如果您想从障碍和重新排序的角度讨论这个问题,volatile
写入 data
保证提交所有先前的写入,其中包括 1
的写入第一个数组槽,但它不保证后续的非 volatile
写入不会更早提交。因此,App
引用的不正确发布仍然有可能在 volatile
写入之前执行(尽管这种情况很少发生)。
如果将写入移动到构造函数的末尾,一旦看到非 null
数组引用,所有先前的写入都可见。对于 final
字段,不需要进一步讨论,如上所述,write 在构造函数中的实际位置无论如何都是无关紧要的。对于 volatile
的情况,如上所述,您不能保证读取非 null
引用,但是当您读取它时,所有先前的写入都会被提交。了解表达式 new int[]{1, 0};
无论如何都会被编译为 hiddenVariable=new int[2]; hiddenVariable[0]=1; hiddenVariable[1]=0;
的等价物可能会有所帮助。在构造之后但在 volatile
写入字段的数组引用之前放置另一个数组写入,不会改变语义。