使用 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-17,final
个字段:
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,但是构造函数中的代码保证看到该字段已初始化。
摘自书 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-17,final
个字段:
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,但是构造函数中的代码保证看到该字段已初始化。