【问题标题】:Use volatile field to publish an object safely使用 volatile 字段安全地发布对象
【发布时间】:2021-06-13 09:57:37
【问题描述】:

来自Java Concurrency In Practice一书:

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

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

我的问题是:

为什么要点 3 有约束:“of a properConstructed object”,但要点 2 没有?

以下代码是否安全地发布了map 实例?我认为代码符合要点2的条件。

public class SafePublish {

    volatile DummyMap map = new DummyMap();

    SafePublish() throws InterruptedException {
        new Thread(new Runnable() {
            @Override
            public void run() {
                // Safe to use 'map'?
                System.out.println(SafePublish.this.map);
            }
        }).start();
        Thread.sleep(5000);
    }

    public static void main(String[] args) throws InterruptedException {
        SafePublish safePublishInstance = new SafePublish();
    }
 

public class DummyMap {
    DummyMap() {
        System.out.println("DummyClass constructing");
      }
  }
} 

以下调试快照图片显示map 实例在执行时为null,正在进入SafePublish 的构造。如果另一个线程现在试图读取map 引用会发生什么?阅读安全吗?

【问题讨论】:

  • 仅供参考:“这里打印 null”不是因为“正确构造的对象”的线程安全问题。这纯粹是因为 this.map 还没有被赋值,因为这要等到 5 秒后才会发生。
  • @Andreas 有两个“正确构造的对象”,第一个是正在发布的对象,第二个是其字段为已发布对象的对象。请仔细阅读。
  • 没关系。正如我已经说过的,该字段为null 的事实与线程安全无关。该字段为null,因为尚未分配该字段。代码如何打印除 null 之外的任何内容?那会是什么非空值?
  • 因为这是条件。但是这个问题是没有意义的,因为你在问为什么代码不是线程安全的,如“由你的代码显示”,但代码没有显示,它只是显示一个尚未分配的字段默认值为null。该代码不能证明任何事情,而是根据您的有缺陷的结论提出问题。
  • @Jason 我不知道您所说的“SafePublish 的构造”是什么意思。如果你反编译构造函数,你会看到编译器把它改成了SafePublish() { super(); this.map = new DummyMap(); new Thread(...,所以你可以看到,map被赋值之前你在构造函数中写的代码,但它确实发生了执行期间构造函数执行作为一个整体。

标签: java concurrency volatile java-memory-model safe-publication


【解决方案1】:

这是因为final 字段保证对其他线程可见只有在对象构造之后,而对volatile 字段的写入可见性保证在没有任何附加条件的情况下。

来自jls-17final 字段:

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

volatile 字段:

对 volatile 变量 v(第 8.3.1.4 节)的写入与任何线程对 v 的所有后续读取同步(其中“后续”根据同步顺序定义)。

现在,关于您的特定代码示例,JLS 12.5 保证在执行构造函数中的代码之前发生字段初始化(请参阅 JLS 12.5 中的步骤 4 和 5,此处引用的时间有点长)。因此,Program Order 保证构造函数的代码将看到 map 已初始化,无论它是 volatilefinal 还是只是一个常规字段。由于在字段写入和线程启动之前存在 Happens-Before 关系,因此即使您在构造函数中创建的线程也会看到 map 已初始化。

请注意,我专门写了“在执行构造函数中的代码之前”而不是“在执行构造函数之前”,因为这不是 JSL 12.5 做出的保证(请阅读!)。这就是为什么您在调试器中在构造函数代码的第一行之前看到 null 的原因,但保证构造函数中的代码可以看到该字段已初始化。

【讨论】:

  • 感谢您的洞察力。请参考我上面给出的代码,代码是否安全地发布了map 实例?如果是这样,那么 volatile map 的构造是否发生在 SafePublish 类的构造之前?
  • 这是一个极端的例子,但我认为它是安全的。如果mapfinal,那么它也是安全的,因为线程是在map 字段被写入之后启动的。如果map 是最终的并且写入它是在线程启动之后,那将是不安全的。
  • @Jason 是的,它是安全的。我在答案中添加了解释。
  • 仅供参考,并发中的“安全”总是意味着两个(或更多)R/W 操作的相对顺序——而不仅仅是“这个单一操作是否安全”。因此,在考虑安全性时,请确定您尝试订购的操作,并以这些术语提出问题!
  • 问题中的代码绝对不是在实践中应该做的事情,但我认为这只是一个说明性的例子。关于 Java 内存模型的问题充满了人为的例子,这些例子在实践中很少出现(或者以更复杂的方式出现,很难以问题的形式出现)。此外,OP 专门询问了以这种方式 发布 的安全性,而不是上述代码的一般理智。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-05-18
  • 2010-12-09
  • 2016-04-28
  • 2012-12-01
  • 1970-01-01
相关资源
最近更新 更多