使用 volatile 字段安全地发布一个对象

Use volatile field to publish an object safely

摘自书 Java 并发实践:

To publish an object safely, both the reference to the object and the object’s state must be made visible to other threads at the same time. A properly constructed object can be safely published by:

  • Initializing an object reference from a static initializer;
  • Storing a reference to it into a volatile field or AtomicReference;
  • Storing a reference to it into a final field of a properly constructed object; or
  • Storing a reference to it into a field that is properly guarded by a lock.

我的问题是:

为什么要点 3 有约束:of a properly constructed 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 引用,会发生什么情况?读书安全吗?

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

来自 jls-17final 个字段:

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.

volatile 个字段:

A write to a volatile variable v (§8.3.1.4) synchronizes-with all subsequent reads of v by any thread (where "subsequent" is defined according to the synchronization order).

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

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