为什么要安全地发布对象,为什么需要“存储对最终字段的引用”和“正确构造的对象”?

问题描述 投票:1回答:1

我正在阅读“实践中的Java并发”,它说:

要安全地发布对象,必须同时使对象的引用和对象的状态对其他线程可见。正确构造的对象可以通过以下方式安全发布:

  • 从静态初始化程序初始化对象引用;
  • 将对它的引用存储到volatile字段或AtomicReference中;
  • 将对它的引用存储到正确构造的对象的最终字段中;要么
  • 将对它的引用存储到由锁正确保护的字段中。

我不明白的是“将它的引用存储到正确构造的对象的final字段中”,为什么需要“正确构造的对象”?如果没有“正确构造的对象”,其他线程是否可以看到以不一致状态发布的对象?

我读过几个相关的问题:

但是我找不到很多解释为什么需要“正确构造的物体”。

下面的例子来自an answer in question "final vs volatile guaranntee w.rt to safe publication of objects - Stack Overflow"

class SomeClass{
    private final SomeType field;

    SomeClass() {
        new Thread(new Runnable() {
            public void run() {
                SomeType copy = field; //copy could be null
                copy.doSomething(); //could throw NullPointerException
            }
        }).start();
        field = new SomeType();
    }
}

SomeClass没有正确构造,当然,copy可能是null,但在我看来,线程不能看到copy处于不一致状态,要么copynull或“两者都提到copycopy的状态必须做同时对线程可见“。所以,field安全发布,即使SomeClass没有正确构建。我对吗?

希望有人能提前给我更多解释,谢谢。

java multithreading java-memory-model safe-publication
1个回答
0
投票

这取决于你所谓的“一致状态”。如果看到一个空指针应该是一个已发布的对象,即该对象实际上看起来好像它没有被发布,则计为“一致”,那么你就是正确的,因为该示例产生了“一致性”。

请注意,final字段应该不会改变它们的值。如果一个线程从final读取,它可以安全地假设该字段的值不会在以后更改。线程的实现或(JIT)编译器可以将字段的值“缓存”在某个变量(或寄存器)中,因为final告诉它读取的值保持不变。

具体来说,代码就像

new Thread(new Runnable() {
    public void run() {
        while ( field == null ) {
          // Wait a little...
        }
        // Field was initialized, go ahead.
    }
}).start();

可能只会有两种可能的结果:循环永远不会进入,或者变成无限循环。

这就是为什么在初始化之前访问final字段尤其不安全;和final字段仅保证在构造函数完成后初始化。

如果在线程代码中编写完全限定的字段名称(SomeClass.this.field),问题可能会变得更加明显。您可以省略它,但编译器将隐式为您生成适当的访问权限。使用完全限定的字段名称,您可以更清楚地看到线程在SomeClass.this.field完全初始化之前访问this。因此,实际上,您实际上并未发布一致的SomeType对象,而是仍然不一致的SomeClass对象恰好包含该字段。

© www.soinside.com 2019 - 2024. All rights reserved.