是否可以在对象创建期间观察最终数组的中间状态?
Could it be possible to observe an intermediate state of a final array during object creation?
想象一下,在我的并发应用程序中,我有一个像这样的 Java class(非常简单):
更新:
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;
}
}
假设一个线程创建此 class 的一个对象,另一个引用此对象的线程从数组 arr
中读取值。
第二个线程是否可以观察数组 arr
的 1, 0
值?
为了检查这个案例,我用 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
- OS: Linux
- OS版本:4.4.7-300.fc23.x86_64
- Java版本:9-ea+123
- OS架构:amd64
- 测试迭代次数:10^10
Let's say one thread creates an object of this class and another thread that has a reference to this object reads values from an array arr.
对于所写的示例,在构造函数 returns 之前这是不可能的。引用不是由构造函数发布的;即它已安全发布。
Is it possible for second thread observe 1, 0 values of the array arr?
没有。由于对象已安全发布,因此 JLS 17.5。保证开始发挥作用:
"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."
通过应用 JLS 17.5.1 的规则,我们可以看到这些保证扩展到在构造函数体末尾冻结之前由构造函数初始化的任何完全封装的对象。
这在 Goetz 等人 "Java: Concurrency in Action".
中也用更易于理解的术语进行了描述
如果您将示例更改为:
public static class Data {
public static Data instance;
final int[] arr;
public Data() {
instance = this;
arr = new int[]{1, 0};
arr[1] = 2;
}
}
我添加的语句改变了一切。现在一些其他线程可以在构造函数完成之前看到一个 Data
实例。然后它可能能够看到处于中间状态的 arr[1]
。
对 Data
实例的引用的 "leakage" 在它仍在构建时 不安全发布。
公理化的 final 字段语义由特殊的 happens-before 规则控制。该规则是(来自我的 JMM Pragmatics, but most of the subsequent explanations there are due to https://whosebug.com/users/1261287/vladimir-sitnikov 的幻灯片):
现在。在初始存储后修改数组元素的示例中,以下是操作与程序的关系:
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)
}
}
w hb F
和 F hb a
微不足道。 a mc r1
(由于a mc read(data)
和read(data) dr read(data.arr)
。最后,r1 dr r2
因为是对数组元素的解引用。构造完成,因此写动作arr[1] = 2
happens-before read 操作 r.r1 = arr[1] (reads 2)
。换句话说,此执行要求在 arr[1]
.
中看到“2”
注意:为了证明所有执行都产生“2”,您必须证明没有执行可以读取初始存储到数组元素。在这种情况下,它几乎是微不足道的:没有执行可以看到数组元素写入并绕过冻结操作。如果有 this
"leakage",这样的执行是很容易构造的。
旁白:请注意,这意味着最终字段存储初始化顺序与最终字段保证无关,只要没有泄漏即可。 (这就是规范在说 "It will also see versions of any object or array referenced by those final fields that are at least as up-to-date as the final fields are." 时所暗示的内容)
想象一下,在我的并发应用程序中,我有一个像这样的 Java class(非常简单):
更新:
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;
}
}
假设一个线程创建此 class 的一个对象,另一个引用此对象的线程从数组 arr
中读取值。
第二个线程是否可以观察数组 arr
的 1, 0
值?
为了检查这个案例,我用 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
- OS: Linux
- OS版本:4.4.7-300.fc23.x86_64
- Java版本:9-ea+123
- OS架构:amd64
- 测试迭代次数:10^10
Let's say one thread creates an object of this class and another thread that has a reference to this object reads values from an array arr.
对于所写的示例,在构造函数 returns 之前这是不可能的。引用不是由构造函数发布的;即它已安全发布。
Is it possible for second thread observe 1, 0 values of the array arr?
没有。由于对象已安全发布,因此 JLS 17.5。保证开始发挥作用:
"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."
通过应用 JLS 17.5.1 的规则,我们可以看到这些保证扩展到在构造函数体末尾冻结之前由构造函数初始化的任何完全封装的对象。
这在 Goetz 等人 "Java: Concurrency in Action".
中也用更易于理解的术语进行了描述如果您将示例更改为:
public static class Data {
public static Data instance;
final int[] arr;
public Data() {
instance = this;
arr = new int[]{1, 0};
arr[1] = 2;
}
}
我添加的语句改变了一切。现在一些其他线程可以在构造函数完成之前看到一个 Data
实例。然后它可能能够看到处于中间状态的 arr[1]
。
对 Data
实例的引用的 "leakage" 在它仍在构建时 不安全发布。
公理化的 final 字段语义由特殊的 happens-before 规则控制。该规则是(来自我的 JMM Pragmatics, but most of the subsequent explanations there are due to https://whosebug.com/users/1261287/vladimir-sitnikov 的幻灯片):
现在。在初始存储后修改数组元素的示例中,以下是操作与程序的关系:
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)
}
}
w hb F
和 F hb a
微不足道。 a mc r1
(由于a mc read(data)
和read(data) dr read(data.arr)
。最后,r1 dr r2
因为是对数组元素的解引用。构造完成,因此写动作arr[1] = 2
happens-before read 操作 r.r1 = arr[1] (reads 2)
。换句话说,此执行要求在 arr[1]
.
注意:为了证明所有执行都产生“2”,您必须证明没有执行可以读取初始存储到数组元素。在这种情况下,它几乎是微不足道的:没有执行可以看到数组元素写入并绕过冻结操作。如果有 this
"leakage",这样的执行是很容易构造的。
旁白:请注意,这意味着最终字段存储初始化顺序与最终字段保证无关,只要没有泄漏即可。 (这就是规范在说 "It will also see versions of any object or array referenced by those final fields that are at least as up-to-date as the final fields are." 时所暗示的内容)