有 Java 编程相关的问题?

你可以在下面搜索框中键入要查询的问题!

java在对象创建过程中是否可以观察到最终数组的中间状态?

想象一下,在我的并发应用程序中,我有这样一个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中读取值。 第二个线程是否可以观察数组arr1, 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
  • 操作系统:Linux
  • 操作系统版本:4.4.7-300。fc23。x86_64
  • Java版本:9-ea+123
  • OS Arch:amd64
  • 测试迭代次数:10^10

共 (2) 个答案

  1. # 1 楼答案

    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.

    对于编写的示例,在构造函数返回之前,这是不可能的。建造商未发布参考文件;i、 e.它是安全出版的

    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》(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实例仍在构建时,引用的“泄漏”是不安全的发布

  2. # 2 楼答案

    公理化的final-field语义由一个特殊的before规则控制。这条规则是(我的JMM Pragmatics中的一张幻灯片,但后面的大多数解释都是由于https://stackoverflow.com/users/1261287/vladimir-sitnikov):

    enter image description here

    现在。在初始存储后修改数组元素的示例中,以下是操作与程序的关系:

    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 FF 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”,必须证明noexecution可以读取数组元素的初始存储。在这种情况下,这几乎是微不足道的:没有执行可以看到数组元素写入并绕过冻结操作。如果存在this“泄漏”,那么这种执行是可以构造的

    旁白:请注意,这意味着最终字段存储初始化顺序与最终字段保证无关,只要没有泄漏。(这是spec在说时提到的内容,“它还将看到那些最终字段引用的任何对象或数组的版本,这些版本至少与最终字段一样最新。”