如果您将对象分配给最终字段,其他线程是否会看到该对象的 non-final/non-volatile 字段的先前更新?

If you assign an Object to a final field, will other threads see previous updates of that Object's non-final/non-volatile fields?

阅读 Java 语言规范,我发现了这段关于最终字段的摘录:

The usage model for final fields is a simple one: Set the final fields for an object in that object's constructor; and do not write a reference to the object being constructed in a place where another thread can see it before the object's constructor is finished. If this is followed, then when the object is seen by another thread, that thread will always see the correctly constructed version of that object's final fields. 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.

Link: https://docs.oracle.com/javase/specs/jls/se8/html/jls-17.html#jls-17.5

我的问题是,"versions" 是否意味着更新?这意味着final字段引用的对象的non-final/non-volatile字段在构建后也会从主存(而不是本地缓存)中读取?


例子

假设 thread #1 创建一个 objectB 并设置其中一个 non-final/non-volatile 字段。

然后 thread #2 将同一字段设置为不同的内容,创建一些其他 objectA,最终字段设置为 objectB,然后将 objectA 放在 thread #1 可以得到。

thread #1 然后获取 objectA,并将其最终字段视为 objectBthread #1 是否可能看不到 thread #2objectB 所做的更改?

或者这里有一些代码显示了我的意思:

public class Test {
    private static final ConcurrentLinkedQueue<A> myAs = new ConcurrentLinkedQueue<>();
    private static long timer = System.nanoTime() + 3000000000L; // 3 seconds into the future

    public static void main(String... args) {
        B myB = new B("thread #1"); // Set in thread 1

        new Thread(() -> {
            myB.setString("thread #2"); // Set in thread 2
            myAs.add(new A(myB));
        }).start();

        for(long i = 0; i < x; i = System.nanoTime()) {} // Busy-wait for about 3 seconds

        System.out.println(myAs.poll().getB().getString()); // Print out value
    }

    public static class A {
        private final B b;

        public A(B b) {
            this.b = b;
        }

        public B getB() {
            return b;
        }
    }

    public static class B {
        private String s = null;

        public B(String s) {
            this.s = s;
        }

        public String getString() {
            return s;
        }

        public void setString(String s) {
            this.s = s;
        }
    }
}

代码似乎读取了更新后的值,但我不确定这是否只是出于偶然。

"Is it possible for thread #1 to not see the changes to objectB made by thread #2?"

是的。因为thread #1可以缓存值。

规范中的引用意味着在分配最终字段值和发布对象之间存在 happened before

这很有趣,我想我很久以前就读过...这是一个例子(如果我没记错的话):

 static class Holder {

    private final String[] names;

    static Holder newHolder;

    public Holder() {
        super();
        names = new String[3];
        names[0] = "first";
        names[1] = "last";
    }

    public void newObject() {
        newHolder = new Holder();
        newHolder.names[2] = "oneMore";
    }

    public void readObject() {
        System.out.println(Arrays.toString(newHolder.names));
    }

}

假设这里涉及两个线程:ThreadAThreadB。现在还假设 ThreadA 调用 newObject;完成后 ThreadB 调用 readObject.

绝对不能保证 ThreadB 会打印 first, last, oneMore;只能保证 firstlast 肯定会出现。

一旦您根据 final fields used inside constructor 中使用的 MemoryBarriers 来思考,这个顺便说一句就完全可以理解了。

在当前的实现中,实际上看起来像这样:

public Holder() {
    super();
    names = new String[3];
    names[0] = "first";
    names[1] = "last";
}
// [StoreStore]
// [LoadStore]

在构造函数的末尾插入了两个屏障,以防止发生其他读取和存储。

恕我直言,您将始终看到更新后的值。这里发生了两件事

  1. 安全发布
  2. 在构造函数结束时冻结动作

因为我们有一个冻结动作,其他线程应该能够看到 myB 的内容

更多详情: https://shipilev.net/blog/2014/jmm-pragmatics/#_part_v_finals

https://www.ibm.com/developerworks/library/j-jtp03304/index.html