Java 是否有可能在对象创建期间观察最终数组的中间状态?
想象一下,在我的并发应用程序中有这样一个Java类(非常简化): 更新:Java 是否有可能在对象创建期间观察最终数组的中间状态?,java,multithreading,concurrency,constructor,final,Java,Multithreading,Concurrency,Constructor,Final,想象一下,在我的并发应用程序中有这样一个Java类(非常简化): 更新: public class Data { static Data instance; final int[] arr; public Data() { arr = new int[]{1, 0}; arr[1] = 2; } public static void main(String[] args) { new Thread(()
public class Data {
static Data instance;
final int[] arr;
public Data() {
arr = new int[]{1, 0};
arr[1] = 2;
}
public static void main(String[] args) {
new Thread(() -> instance = new Data()).start();
System.out.println(Arrays.toString(instance.arr));
}
}
不正确:
public static class Data {
final int[] arr;
public Data() {
arr = new int[]{1, 0};
arr[1] = 2;
}
}
假设一个线程创建此类的对象,另一个线程引用该对象,从数组arr
读取值。
第二个线程是否可以观察数组的值arr
为了检查这个案例,我已经使用JCStress框架编写了测试(感谢@AlekseyShipilev):
在下面的评论之后,测试似乎也不正确
@JCStressTest
@Outcome(id = "2, 1", expect = Expect.ACCEPTABLE, desc = "Seeing the set value.")
@Outcome(expect = Expect.FORBIDDEN, desc = "Other values are forbidden.")
@State
public class FinalArrayTest {
Data data;
public static class Data {
final int[] arr;
public Data() {
arr = new int[]{1, 0};
arr[1] = 2;
}
}
@Actor
public void actor1() {
data = new Data();
}
@Actor
public void actor2(IntResult2 r) {
Data d = this.data;
if (d == null) {
// Pretend we have seen the set value
r.r1 = 2;
r.r2 = 1;
} else {
r.r1 = d.arr[1];
r.r2 = d.arr[0];
}
}
}
在我的机器上,第二个线程总是观察最后一个赋值arr[1]=2
,但我仍然怀疑,在像ARM这样的所有平台上,我会得到相同的结果吗
所有测试均在具有此配置的计算机上执行:
- 芯数:4
- Java供应商:Oracle
- 操作系统:Linux
- 操作系统版本:4.4.7-300.fc23.x86_64
- Java版本:9-ea+123
- OS-Arch:amd64
- 测试迭代次数:10^10
如果要将示例更改为:
public static class Data {
public static Data instance;
final int[] arr;
public Data() {
instance = this;
arr = new int[]{1, 0};
arr[1] = 2;
}
}
我添加的语句改变了一切。现在,在构造函数完成之前,其他一些线程可以看到一个数据实例。然后,它可以看到处于中间状态的arr[1]
当数据
实例仍在构造时,引用的“泄漏”是不安全的发布。公理化的final字段语义受特殊的before规则控制。这条规则是(我的幻灯片,但后面的大部分解释都是由于):
现在。在初始存储后修改数组元素的示例中,以下是操作与程序的关系:
public class FinalArrayTest {
Data data;
public static class Data {
final int[] arr;
public Data() {
arr = new int[]{1, 0};
arr[1] = 2; // (w)
} // (F)
}
@Actor
public void actor1() {
data = new Data(); // (a)
}
@Actor
public void actor2(IntResult1 r) {
// ignore null pointers for brevity
Data d = this.data;
int[] arr = d.arr; // (r1)
r.r1 = arr[1]; // (r2)
}
}
whbf
和fhba
微不足道mc r1
(由于a mc read(data)
和read(data)dr read(data.arr)
。最后,r1 dr r2
,因为它是数组元素的解引用。构造完成,因此写入操作arr[1]=2
发生在读取操作r.r1=arr[1](读取2)之前
。换句话说,此执行要求在arr[1]
中看到“2”
注意:为了证明所有执行都产生“2”,您必须证明没有执行可以读取数组元素的初始存储。在这种情况下,这几乎是微不足道的:没有执行可以看到数组元素写入并绕过冻结操作。如果出现此
“泄漏”,这样的执行是可以构造的
旁白:请注意,这意味着只要没有泄漏,最终字段存储初始化顺序与最终字段保证无关。(这是规范在说“它还将看到这些最终字段引用的任何对象或数组的版本,这些版本至少与最终字段一样最新。”)特别是,您担心JIT可能会重新排序另一个线程的指令,该线程获取新的数据引用并使用它,而数据构造函数执行arr[1]=2;
?(因为JIT在某些情况下可以对事物进行重新排序。我相信,和/或编译器,但我认为它主要是JIT。)在对象创建完成之前,对对象的引用不会设置为data
变量。因此,您永远不会看到arr[1]==0
@marstran:由于编译器和/或JIT可以对指令进行重新排序,您可以提供任何引用作为备份吗?JLS保证通过以最终字段为根的取消引用链访问的所有状态将至少与构造函数完成时一样最新。冻结操作发生在构造函数c完成。正如T.J.Crowder回答我上面类似的评论时所说的那样,您是否有任何支持这一观点的参考文献?Goetz等人“Java并发在实践中”.第16.2.2章。您假定实例的发布是安全的?在OP的示例中,实例>>is@INlHELL-您更新的问题中的示例是安全的。实例
的赋值发生在数据
构造函数完成后。只有在构造函数之前泄漏引用,您才会得到不安全的发布完成。一如既往,这是一个很好的完整解释!这个特殊的句子是规范中最容易混淆的句子之一,只有当你开始考虑在构建后通过“特殊机制”进行最后的字段更新时,它的相关性才变得明显@AlekseyShipilev因此,如果引用在对象构造期间泄漏,我们可以观察到arr
field-[0,0][1,0]或[1,2]的任何中间状态,对吗?这句话描述:“它还将看到任何对象或数组的版本。”