想象一下,在我的并发应用程序中,我有一个像这样的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(() -> 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
读取值。
第二个线程是否可以观察数组1, 0
的值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等所有平台上获得相同的结果吗?
所有测试均在具有此配置的计算机上执行:
答案 0 :(得分:4)
假设一个线程创建了这个类的对象,另一个具有对该对象的引用的线程从数组arr读取值。
使用编写的示例,在构造函数返回之前,这是不可能的。引用不是由构造函数发布的;即安全发布。
第二个线程是否可以观察数组arr的1,0值?
没有。由于该对象已安全发布,因此JLS 17.5。保证发挥作用:
"当对象的构造函数完成时,它被认为是完全初始化的。在该对象完全初始化之后只能看到对象引用的线程可以保证看到该对象的最终字段的正确初始化值。"
通过应用JLS 17.5.1的规则,我们可以看到这些保证扩展到构造函数初始化的构造函数初始化的任何完全封装的对象。
在 Goetz等人" Java:并发行动"
中,更容易理解这一术语。如果您要将示例更改为:
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
实例的引用仍在构建中 unsafe publication 。
答案 1 :(得分:1)
公理化的最终字段语义由特殊的先发条件规则控制。该规则是(来自JMM Pragmatics的幻灯片,但大部分后续解释都归因于https://stackoverflow.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
在阅读行动r.r1 = arr[1] (reads 2)
之前发生。换句话说,此执行要求在arr[1]
中查看" 2"
注意:为了证明所有执行都在产生" 2",您必须证明 no 执行可以读取初始存储到数组元素。在这种情况下,它几乎是微不足道的:没有执行可以看到数组元素写入并绕过冻结操作。如果有this
"泄漏",这样的执行很容易构建。
除此之外:请注意,这意味着最终的字段存储初始化顺序与最终字段保证无关,只要没有泄漏。 (这是规范中提到的"它还会看到那些最终字段所引用的任何对象或数组的版本,这些字段至少与最终字段一样是最新的。&#34 ; )