使用 volatile 字段安全发布对象

Jas*_*son 3 java concurrency volatile java-memory-model safe-publication

来自Java Concurrency In Practice一书:

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

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

我的问题是:

为什么要点 3 有约束:“正确构造的对象”,而要点 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");
      }
  }
} 
Run Code Online (Sandbox Code Playgroud)

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

在此处输入图片说明

Mal*_*alt 6

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

jls-17,在final字段上:

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

volatile领域:

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

现在,关于您的特定代码示例,JLS 12.5保证在执行构造函数中的代码之前发生字段初始化(请参阅 JLS 12.5 中的步骤 4 和 5,此处引用有点太长)。因此,程序的顺序保证构造函数的代码会看到map初始化,无论它是volatilefinal或只是一个普通的领域。由于在字段写入和线程开始之前存在Happens-Before关系,即使您在构造函数中创建的线程也会被map视为已初始化。

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