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(()

想象一下,在我的并发应用程序中有这样一个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
读取值。 第二个线程是否可以观察数组的值
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
假设一个线程创建这个类的对象,另一个线程引用这个对象,从数组arr读取值

对于编写的示例,在构造函数返回之前,这是不可能的。该引用不是由构造函数发布的;i、 它是安全出版的

第二个线程是否可以观察数组arr的1,0值

不可以。由于对象已安全发布,因此此。担保开始发挥作用:

“当一个对象的构造函数完成时,该对象被视为已完全初始化。只有在该对象完全初始化后才能看到对该对象的引用的线程才能确保看到该对象最终字段的正确初始化值。”

通过应用的规则,我们可以看到这些保证扩展到在构造函数体的末尾冻结之前由构造函数初始化的任何完全封装的对象

在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;
    }
}
我添加的语句改变了一切。现在,在构造函数完成之前,其他一些线程可以看到一个
数据
实例。然后,它可以看到处于中间状态的
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]的任何中间状态,对吗?这句话描述:“它还将看到任何对象或数组的版本。”