问题描述
摘自Java并发实践一书:
要安全地发布对象,必须同时使对对象的引用和对象的状态对其他线程可见。正确构造的对象可以通过以下方式安全地发布:
- 从静态初始化器初始化对象引用;
- 将对其的引用存储到 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");
}
}
}
以下调试快照图片显示 map
实例在执行时为 null
,正在进入 SafePublish
的构造。如果另一个线程正在尝试读取 map
引用,会发生什么情况?阅读安全吗?
解决方法
这是因为保证 final
字段对其他线程可见仅在对象构造之后,而对 volatile
字段写入的可见性保证没有任何附加条件。
从 jls-17,在 final
字段:
当一个对象的构造函数完成时,它被认为是完全初始化的。只有在对象完全初始化后才能看到对该对象的引用的线程可以保证看到该对象的最终字段的正确初始化值。
在 volatile
字段上:
对易失性变量 v 的写入(第 8.3.1.4 节)与任何线程对 v 的所有后续读取同步(其中“后续”根据同步顺序定义)。
现在,关于您的具体代码示例,JLS 12.5 保证在执行构造函数中的代码之前进行字段初始化(请参阅 JLS 12.5 中的第 4 步和第 5 步,此处引用有点太长)。因此,Program Order 保证构造函数的代码将看到 map
已初始化,无论它是 volatile
或 final
还是只是一个常规字段。由于在字段写入和线程开始之前存在 Happens-Before 关系,即使您在构造函数中创建的线程也会看到 map
已初始化。
请注意,我专门写了“在执行构造函数中的代码之前”而不是“在执行构造函数之前”,因为这不是 JSL 12.5 做出的保证(阅读它!)。这就是为什么您在构造函数代码的第一行之前在调试器中看到 null 的原因,但保证构造函数中的代码看到该字段已初始化。